请求发送和接收的延迟。
道理很简单,客户端发出一个请求,100 ms后收到服务器答复:服务器在00:00:30.000时收到请求,在00:00:30.006时返回请求。客户端怎么知道自己要把时间设置到什么地方呢?
显然,没有任何软件手段能获知请求的去程和响应的回程在路上传输的时间。
所以,我们必须引入一个核心假设:去程和返程的延迟是相同的。
这很合理,因为一般而言往返走的线路不会有太大差异,但是也引入了 NTP 的最大误差,幸运的是,通过多次请求和寻找最佳服务器线路,精度可控制在亚毫秒级别。
若你想了解生产生活中人们如何获取更高的精度的授时,可以搜索:
PTP,GNSS/GPS 授时技术。
回到具体的例子,我们可以发现,“在路上”的请求时间是100-6=94 ms,通过假设计算得单程延迟94/2=47 ms,所以我们只需将本地时间设置为:00:00:30.006 + 0.047 = 00:00:30.053即可。
前端的思路就是模仿这种策略,吸取其核心假设:
去程和返程的延迟是相同的。
那么,时间同步的服务去哪里找?自己后端搭一个当然可以,但是前提是保证服务器的时间准确。有没有现成的?我询问了各大 AI,给了我很多网站,有的需要认证,有的已经停止维护。我进行了逐家验证,最后向大家我推荐两个截至发文无需认证免费使用、较为稳定的后端时间请求 API:
https://api.shijian.online/timestamp/。它的返回格式如下:{"status":1,"data":{"timestamp":1780057227741}}https://timeapi.io/api/Time/current/zone?timeZone=UTC。它的返回格式如下:{"year":2026,"month":5,"day":30,"hour":13,"minute":17,"seconds":51,"milliSeconds":112,"dateTime":"2026-05-30T13:17:51.1120409","date":"05/30/2026","time":"13:17","timeZone":"UTC","dayOfWeek":"Saturday","dstActive":false}
都至少精确到毫秒。所以我们比较服务器接受到请求的那一刻的本地和云端时间:
- 本地时间:
(start_time + end_time) / 2(估计,最大误差为(end_time - start_time) / 2);云端时间:API 返回的时间。 - 如能访问成功两个同步服务器,我们选择
逆方差加权(如有想了解的读者可自行搜索)。简单来说,延迟越低,可信度(权重)呈平方级增长。 - 为防止超时,我们将超时设为 1.5s,因为长时间低精度的时间同步本就失去了意义。
- 尤其注意时区处理和校准调时的前后方向,重点测试,不要闹笑话,一下子差掉几个小时,或者越调越偏。
代码实现
interface TimeSyncProvider { |
name: string; |
url: string; |
toTimestamp(json: any): number; |
} |
interface SyncResult { |
drift: number; // 定义偏移量 = 服务器时间 - 本地时间 |
rtt: number; // 往返延迟 (Round-Trip Time) |
} |
/** |
* 获取单个源的时间偏移 |
*/ |
async function getOneDrift(provider: TimeSyncProvider): Promise<SyncResult | null> { |
const start = Date.now(); |
try { |
const response = await fetch(provider.url, { |
cache: 'no-store', |
signal: AbortSignal.timeout(1500) // 1.5s 超时断开 |
}); |
const json = await response.json(); |
const end = Date.now(); |
const serverTime = provider.toTimestamp(json); |
const rtt = end - start; |
// 核心逻辑:假设服务器收到请求的时刻是 (start + end) / 2 |
// drift = serverTime - (start + end) / 2 |
const drift = serverTime - (start + rtt / 2); |
return { drift, rtt }; |
} catch (e) { |
console.warn(`同步源 ${provider.name} 请求失败:`, e); |
return null; |
} |
} |
/** |
* 逆方差加权融合多个源的结果 |
*/ |
async function getWeightedDrift(providers: TimeSyncProvider[]): Promise<number | null> { |
const results = await Promise.all(providers.map(p => getOneDrift(p))); |
const validResults = results.filter((r): r is SyncResult => r !== null); |
if (validResults.length === 0) return null; |
// 逆方差加权逻辑:权重 w = 1 / (rtt^2)。延迟越低,可信度呈平方级增长。 |
let totalWeight = 0; |
let weightedDrift = 0; |
validResults.forEach(res => { |
const weight = 1 / Math.pow(Math.max(res.rtt, 1), 2); |
weightedDrift += res.drift * weight; |
totalWeight += weight; |
}); |
return weightedDrift / totalWeight; |
} |
const providers: TimeSyncProvider[] = [ |
{ |
name: "shijian.online", |
url: "https://api.shijian.online/timestamp/", |
toTimestamp: (json) => json.data.timestamp |
}, |
{ |
name: "timeapi.io", |
url: "https://timeapi.io/api/Time/current/zone?timeZone=UTC", |
toTimestamp: (json) => new Date(json.dateTime).getTime() |
} |
]; |
getWeightedDrift(providers).then(finalDrift => { |
console.log(`最终计算得到的本地时间偏差: ${finalDrift.toFixed(2)}ms`); |
// 展示时间 = Date.now() + finalDrift |
}); |
误差来源分析
读者有没有好奇,为什么我们一直在说这种方式是“粗略”的?难道说,NTP 和我们 HTTP 走的不是同一条线路?