简单理解 NTP v3 协议

前段时间瞎折腾,给自己的黑莓 Bold 9900 写了个通过 NTP 同步时间的小工具,顺便在这里记录一下我在实现一个 NTP 客户端时对这个协议的理解。

端口号

NTP 协议使用 UDP 作为传输层协议,服务器监听 UDP 端口 123,在收到有效的报文后,服务器会发送响应报文,否则服务器将直接忽略不做响应。

时间格式

NTP 协议使用三种时间格式。

NTP 短时间格式

短时间格式长度为 32 位,其中高 16 位代表从 NTP 时间戳 0 秒至现在的秒数,低 16 位代表 1 秒以内的分数部分。
这个格式只会在 NTP 报文的 delay 和 dispersion 字段中用到。

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seconds | Fraction |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

NTP 时间戳

NTP 时间戳格式长度为 64 位,其中高 32 位代表从 NTP 时间戳 0 秒至现在的秒数,低 32 位代表 1 秒以内的分数部分。

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seconds |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Fraction |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

NTP 日期格式

NTP 日期格式长度为 128 位,其中高 32 位用来表示 NTP 时间纪元,然后用 32 位表示从当前纪元开始经过的秒数,最后用 64 位表示 1 秒以内的分数部分。

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Era Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Era Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Fraction |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

报文格式

一个 NTP v3 的报文必须包含如下字段:

  • LI - Leap Indicator,2 bit 整型数,指示当月最后一分钟是否包含闰秒
  • VN - Version Number,3 bit 整型数,指示 NTP 协议的版本号。如 NTP v3 就是 3。
  • MODE - 3 bit 整型数,指示发包方的工作模式。通常来说客户端使用 3 (client) 请求时间,服务端使用 4 (server) 返回时间。
  • STRATUM - 8 bit 整型数,代表 NTP 层数。0 代表时钟源,如装备有 GPS 接收机的主服务器;1-15 逐层作为下游服务器,16 被定义为 “无法同步”。
  • POLL - 8 bit 有符号整型数,代表在间隔多少秒后再进行下一次同步。值由 log2(second) 计算得出。
  • PRECISION - 8 bit 有符号整型数,代表系统时钟的精确度。
  • ROOT DELAY - NTP 短时间格式,指示从客户端到根服务器 (stratum 1 的服务器) 的延迟。
  • ROOT DISPERSION - NTP 短时间格式,指示数据从根服务器到客户端之间可能引入的误差。
  • REFERENCE ID - 32 bit 代码,用于标识一个特定的服务器,或一个参考时钟。
    • 对于 stratum 0 的数据包,该字段为 4 个 ASCII 字符,称作 “kiss code”,用于调试和监控。
    • 对于 stratum 1 的数据包,该字段为参考时钟的标识符。标识符由 IANA 维护,此外以 “X” 开头的标识符都被预留给未注册的试验和开发用途。
    • 对于 stratum 2~15 的数据包,该字段为服务器的标识符。当服务器使用 IPv4 时,该字段为服务器的 IP 地址;当服务器使用 IPv6 时,该字段为 IPv6 地址的前四段。
  • REFERENCE TIMESTAMP - NTP 时间戳格式,内容为客户端最后同步的时间。
  • ORIGIN TIMESTAMP - NTP 时间格式,内容为数据包离开客户端的时间。
  • RECEIVE TIMESTAMP - NTP 时间格式,内容为数据包抵达服务器的时间。
  • TRANSMIT TIMESTAMP - NTP 时间格式,内容为数据包离开服务器的时间。
  • DESTINATION TIMESTAMP - NTP 时间格式,内容为数据包抵达客户端的时间。
    • 注:DESTINATION TIMESTAMP 并不会包含在数据包中,而是在客户端收到数据包之后,它的数值才会被确定。

那么全部组合起来,就是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Reference Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Origin Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Receive Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Transmit Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

然而上述字段并不需要全部填写数据,实际上除了 LI、VN、MODE、STRATUM 之外,剩下的所有字段都可以填零。如下就是一个我用来测试的数据包:

1
2
HEX:
DB 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

拆开来看的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BIN:
LI = 0b11 = 3 unknown (clock unsyncronized)
VN = 0b011 = 3
MODE = 0b011 = 3 client
STRATUM = 0b00010000 = 16
POLL = 0b00000000 = 0
PRECISION = 0b00000000 = 0
ROOT DELAY = 0b00000000000000000000000000000000
ROOT DISPERSION = 0b00000000000000000000000000000000
REFERENCE ID = 00000000000000000000000000000000
REFERENCE TIMESTAMP = 00000000000000000000000000000000 00000000000000000000000000000000
ORIGIN TIMESTAMP = 00000000000000000000000000000000 00000000000000000000000000000000
RECEIVE TIMESTAMP = 00000000000000000000000000000000 00000000000000000000000000000000
TRANSMIT TIMESTAMP = 00000000000000000000000000000000 00000000000000000000000000000000

计算 second 和 fraction

计算 second 很简单,取出 timestamp 的高 32 位就可以了;但是从 fraction 计算毫秒数比较麻烦,需要通过 fraction * 10^6 / 2^32 计算得到毫秒数。

这里我给出一个 Java 的代码片段:

1
2
3
4
5
final long seconds = (ntpTimestamp >>> 32) & 0xFFFFFFFFL;
final long secondsInMilliseconds = seconds * 1000;

final long fractionInTimestamp = (ntpTimestamp & 0xFFFFFFFFL);
final long milliseconds = fractionInTimestamp * Math.pow(10, 6) / Math.pow(2, 32);

然后计算 1900 年 1 月 1 日 00:00:00 的 UNIX 时间戳作为基准 UNIX 时间戳,再加上 secondsInMillisecondsmilliseconds,就可以得到 NTP 返回的当前时间了。

参考文档

  • Network Time Protocol Version 4: Protocol and Algorithms Specification - RFC
  • Network Time Protocol (NTP) 网络时间协定 - Jan Ho 的网络世界
  • The Root of All Timing: Understanding root delay and root dispersion in NTP
  • NTP Timestamp - Thompson’s Technological Insight
  • A Very Short Introduction to NTP Timestamps
  • NtpPacketUtils#getNtpTimestampMilliseconds - blackberry_time_sync_ntp - GitHub