前段时间瞎折腾,给自己的黑莓 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 时间戳,再加上 secondsInMilliseconds 和 milliseconds,就可以得到 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