跳到內容
主控台

時間同步

/ntp 回傳伺服器收到請求與送出回應的時間(Unix 毫秒,精確到微秒)。伺服器每 30 秒與上游 NTP 伺服器對時,回傳的時間已經過校正。用戶端記下自己送出與收到的時間,就能用 NTP 的公式算出本機時鐘的偏差,適合需要毫秒級時間的應用(例如比對地震資料的時間戳)。

目前不需要 API Token。

GEThttps://api.lb.exptech.dev/ntp
請求範例
curl "https://api.lb.exptech.dev/ntp"
回應

成功(text/plain)。本文是 T3:送出回應時的伺服器時間(與 X-NTP-T3 相同),Unix 毫秒、小數 3 位,結尾有換行

1794198117123.457
試試看

Token 只會從你的瀏覽器直接送到 API。

本文只有 T3,完整的時間資訊在回應 header:

欄位類型說明
X-NTP-T2stringT2:伺服器收到請求的時間,Unix 毫秒,小數 3 位(例如 1794198117123.412)。
X-NTP-T3stringT3:伺服器送出回應的時間,格式同上。
X-NTP-Statusstring伺服器自身的對時狀態:synced(已對時)、unsynced(啟動後尚未對時成功)、error(連續對時失敗)。不是 synced 時,T2、T3 可能沒有經過校正。

設:

  • T1:用戶端送出請求的時間(本機時鐘)
  • T2:伺服器收到請求的時間(X-NTP-T2)
  • T3:伺服器送出回應的時間(X-NTP-T3)
  • T4:用戶端收到回應的時間(本機時鐘)

四個值都用毫秒。

偏差 offset = ((T2 − T1) + (T3 − T4)) / 2
延遲 delay = (T4 − T1) − (T3 − T2)
標準時間 ≈ 本機時間 + offset
  • offset 為正,表示本機時鐘比伺服器慢。
  • delay 是網路往返時間(扣掉伺服器處理時間)。去程與回程不對稱時,offset 的誤差最多 delay / 2,所以 delay 越小的樣本越準。

建議做法:

  1. 連續取 5–10 個樣本,丟掉第一個(含建立連線的時間,去回程最不對稱)。
  2. 丟掉 X-NTP-Status 不是 synced 的樣本。
  3. 取 delay 最小的樣本的 offset。
  4. T1、T4 用單調時鐘量測(例如 performance.timeOrigin + performance.now()),避免量測途中系統時間被調整。

依上面的建議做法實作對時:連續取樣、丟掉第一個樣本,取 delay 最小的樣本的 offset。

Node.js 18 以上,伺服器端可以直接讀 header:

// ntp.mjs — 執行:node ntp.mjs
const 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(); // 第一次包含建立連線的時間,丟掉
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) / 2
delay ≈ T4 − T1
const 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),一次同步固定用同一個節點。