時刻同期
/ntp は、サーバーがリクエストを受け取った時刻とレスポンスを送った時刻を返します(Unix ミリ秒。マイクロ秒まで)。サーバーは 30 秒ごとに上流の NTP サーバーと時刻を合わせ、返す時刻は補正済みです。クライアントが自分の送受信時刻を記録すれば、NTP の式でローカル時計のずれを計算できます。ミリ秒単位の時刻が必要な用途(地震データのタイムスタンプを比べるなど)に向いています。
現在、API Token は不要です。
サーバー時刻の取得
Section titled “サーバー時刻の取得”GET
https://api.lb.exptech.dev/ntp curl "https://api.lb.exptech.dev/ntp"curl "https://api.lb-tpe1.exptech.dev/ntp"curl "https://api.lb-khh1.exptech.dev/ntp"const res = await fetch("https://api.lb.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());const res = await fetch("https://api.lb-tpe1.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());const res = await fetch("https://api.lb-khh1.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());import requests
res = requests.get("https://api.lb.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)import requests
res = requests.get("https://api.lb-tpe1.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)import requests
res = requests.get("https://api.lb-khh1.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)成功(text/plain)。本文は T3、つまりレスポンスを送ったときのサーバー時刻(X-NTP-T3 と同じ)です。Unix ミリ秒、小数第 3 位、末尾に改行があります
1794198117123.457試してみる
レスポンスのフィールド
Section titled “レスポンスのフィールド”本文は T3 だけで、完全な時刻情報はレスポンス header にあります。
| フィールド | 型 | 説明 |
|---|---|---|
X-NTP-T2 | string | T2:サーバーがリクエストを受け取った時刻。Unix ミリ秒、小数第 3 位(例:1794198117123.412)。 |
X-NTP-T3 | string | T3:サーバーがレスポンスを送った時刻。形式は上と同じです。 |
X-NTP-Status | string | サーバー自身の時刻合わせの状態:synced(合わせ済み)、unsynced(起動後まだ成功していない)、error(時刻合わせが連続して失敗)。synced でないとき、T2 と T3 は補正されていないことがあります。 |
時計のずれの計算
Section titled “時計のずれの計算”次のように置きます。
- T1:クライアントがリクエストを送った時刻(ローカル時計)
- T2:サーバーがリクエストを受け取った時刻(
X-NTP-T2) - T3:サーバーがレスポンスを送った時刻(
X-NTP-T3) - T4:クライアントがレスポンスを受け取った時刻(ローカル時計)
4 つの値はどれもミリ秒です。
ずれ offset = ((T2 − T1) + (T3 − T4)) / 2遅延 delay = (T4 − T1) − (T3 − T2)
標準時刻 ≈ ローカル時刻 + offsetoffsetが正のとき、ローカル時計はサーバーより遅れています。delayはネットワークの往復時間です(サーバーの処理時間を除く)。往路と復路が非対称なとき、offsetの誤差は最大でdelay / 2です。したがってdelayが小さい標本ほど正確です。
推奨する手順:
- 5–10 個の標本を連続で取り、最初の 1 個は捨てます(接続の確立を含むため、往路と復路が最も非対称です)。
X-NTP-Statusがsyncedでない標本は捨てます。delayが最小の標本のoffsetを取ります。- T1 と T4 は単調時計で測ります(例:
performance.timeOrigin + performance.now())。測定の途中でシステム時刻が調整されるのを避けます。
上の推奨手順で時刻を合わせます。連続で標本を取り、最初の標本を捨て、delay が最小の標本の offset を取ります。
Node.js
Section titled “Node.js”Node.js 18 以上。サーバー側は header を直接読めます。
// ntp.mjs — 実行:node ntp.mjsconst BASE = "https://api.lb.exptech.dev";const now = () => performance.timeOrigin + performance.now();
async function sample() { const t1 = now(); const res = await fetch(`${BASE}/ntp`, { cache: "no-store" }); await res.text(); const t4 = now(); const t2 = Number(res.headers.get("X-NTP-T2")); const t3 = Number(res.headers.get("X-NTP-T3")); return { status: res.headers.get("X-NTP-Status"), offset: (t2 - t1 + (t3 - t4)) / 2, delay: t4 - t1 - (t3 - t2), };}
async function syncClock(count = 8) { await sample(); // 最初の 1 回は接続の確立を含むので捨てる const samples = []; for (let i = 0; i < count; i++) { const s = await sample(); if (s.status === "synced") samples.push(s); } if (samples.length === 0) throw new Error("沒有可用的樣本"); return samples.reduce((best, s) => (s.delay < best.delay ? s : best));}
const { offset, delay } = await syncClock();console.log(`本機時鐘偏差 ${offset.toFixed(1)} ms(往返 ${delay.toFixed(1)} ms)`);console.log("校正後時間", new Date(Date.now() + offset).toISOString());サーバーの処理時間は通常 1 ミリ秒よりずっと小さいので、T2 ≈ T3 とみなせます。式は次のように簡略化できます。
offset ≈ T3 − (T1 + T4) / 2delay ≈ T4 − T1const now = () => performance.timeOrigin + performance.now();
async function sample() { const t1 = now(); const res = await fetch("https://api.lb.exptech.dev/ntp", { cache: "no-store" }); const t3 = Number(await res.text()); const t4 = now(); return { offset: t3 - (t1 + t4) / 2, delay: t4 - t1 };}
async function syncClock(count = 8) { await sample(); // 最初の標本は捨てる let best = null; for (let i = 0; i < count; i++) { const s = await sample(); if (!best || s.delay < best.delay) best = s; } return best;}
const { offset, delay } = await syncClock();console.log(`本機時鐘偏差 ${offset.toFixed(1)} ms(往返 ${delay.toFixed(1)} ms)`);このやり方では、X-NTP-Status が synced でない標本を除外できません。
- 時刻の単位はミリ秒です(秒ではありません)。小数第 3 位です。
- リクエストには
cache: "no-store"(またはCache-Control: no-cache)を付け、キャッシュされた古い時刻を受け取らないようにしてください。 - 頻繁に呼ぶ必要はありません。一度合わせたあとは、ローカルの単調時計に
offsetを足して計算すれば足ります。数分ごとに補正し直すことを推奨します。 - これは HTTP 上の推定で、誤差の上限はその標本の
delay / 2です。ネットワークが遅いほど、往路と復路が非対称なほど、誤差は大きくなります。より高い精度が必要なときは、システムの NTP クライアントを使ってください。 - 特定のノードの Base URL(
https://api.lb-tpe1.exptech.dev、https://api.lb-khh1.exptech.dev)を選んでもかまいません。1 回の同期では同じノードを使い続けてください。
