你好,哈布拉用户。 今天我想谈谈如何编写自己的简单的 NTP 客户端。 基本上,对话将转向数据包的结构以及如何处理来自 NTP 服务器的响应。 代码将用 python 编写,因为在我看来,对于此类事情来说,根本没有更好的语言。 行家会注意到代码与 ntplib 代码的相似性——我受到了它的“启发”。
那么 NTP 到底是什么? NTP 是一种与时间服务器通信的协议。 许多现代机器都使用该协议。 例如,Windows 上的 w32tm 服务。
NTP协议共有5个版本。 第一个版本 0(1985 年,RFC958)目前被认为已过时。 目前使用较新的版本:第 1 版(1988 年,RFC1059)、第 2 版(1989 年,RFC1119)、第 3 版(1992 年,RFC1305)和第 4 版(1996 年,RFC2030)。 版本 1-4 彼此兼容,它们的区别仅在于服务器的算法。
数据包格式
跳跃指示器 (校正指示器)是指示闰秒警告的数字。 意义:
- 0 - 不修正
- 1 - 当天的最后一分钟包含 61 秒
- 2 - 当天的最后一分钟包含 59 秒
- 3 - 服务器故障(时间不同步)
版本号 (版本号) – NTP 协议版本号 (1-4)。
时尚 (mode) — 数据包发送方的操作模式。 值从 0 到 7,最常见:
- 3 - 客户端
- 4 - 服务器
- 5-广播模式
阶层 (分层级别)- 服务器和参考时钟之间的中间层数(1 - 服务器直接从参考时钟获取数据,2 - 服务器从级别 1 的服务器获取数据,依此类推)。
泳池 是一个有符号整数,表示连续消息之间的最大间隔。 NTP 客户端在此指定其期望轮询服务器的时间间隔,而 NTP 服务器则指定其期望轮询的时间间隔。 该值等于秒的二进制对数。
平台精度 (精度)是一个有符号整数,表示系统时钟的精度。 该值等于秒的二进制对数。
根延迟 (服务器延迟)– 时钟读数到达 NTP 服务器所需的时间,以定点秒数表示。
根分散 (服务器分散)- NTP 服务器时钟的分散(以定点秒数表示)。
参考号 (源 id) – 观看 id。 如果服务器具有层 1,则 ref id 是原子钟的名称(4 个 ASCII 字符)。 如果服务器使用其他服务器,则 ref id 包含该服务器的地址。
最后 4 个字段是时间 - 32 位 - 整数部分,32 位 - 小数部分。
参考文献 - 服务器上的最新时钟。
起源 – 数据包发送的时间(由服务器填写 – 下面详细介绍)。
收集现金 – 服务器收到数据包的时间。
发送 – 数据包从服务器发送到客户端的时间(由客户端填写,更多内容见下文)。
最后两个字段将不被考虑。
让我们编写我们的包:
封装代码
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Necessary of enter leap second (2 bits)
self.leap_indicator = 0
# Version of protocol (3 bits)
self.version_number = version_number
# Mode of sender (3 bits)
self.mode = mode
# The level of "layering" reading time (1 byte)
self.stratum = 0
# Interval between requests (1 byte)
self.pool = 0
# Precision (log2) (1 byte)
self.precision = 0
# Interval for the clock reach NTP server (4 bytes)
self.root_delay = 0
# Scatter the clock NTP-server (4 bytes)
self.root_dispersion = 0
# Indicator of clocks (4 bytes)
self.ref_id = 0
# Last update time on server (8 bytes)
self.reference = 0
# Time of sending packet from local machine (8 bytes)
self.originate = 0
# Time of receipt on server (8 bytes)
self.receive = 0
# Time of sending answer from server (8 bytes)
self.transmit = transmit
要将数据包发送(和接收)到服务器,我们必须能够将其转换为字节数组。
对于此(和反向)操作,我们将编写两个函数 - pack() 和 unpack():
打包功能
def pack(self):
return struct.pack(NTPPacket._FORMAT,
(self.leap_indicator << 6) +
(self.version_number << 3) + self.mode,
self.stratum,
self.pool,
self.precision,
int(self.root_delay) + get_fraction(self.root_delay, 16),
int(self.root_dispersion) +
get_fraction(self.root_dispersion, 16),
self.ref_id,
int(self.reference),
get_fraction(self.reference, 32),
int(self.originate),
get_fraction(self.originate, 32),
int(self.receive),
get_fraction(self.receive, 32),
int(self.transmit),
get_fraction(self.transmit, 32))
解包函数
def unpack(self, data: bytes):
unpacked_data = struct.unpack(NTPPacket._FORMAT, data)
self.leap_indicator = unpacked_data[0] >> 6 # 2 bits
self.version_number = unpacked_data[0] >> 3 & 0b111 # 3 bits
self.mode = unpacked_data[0] & 0b111 # 3 bits
self.stratum = unpacked_data[1] # 1 byte
self.pool = unpacked_data[2] # 1 byte
self.precision = unpacked_data[3] # 1 byte
# 2 bytes | 2 bytes
self.root_delay = (unpacked_data[4] >> 16) +
(unpacked_data[4] & 0xFFFF) / 2 ** 16
# 2 bytes | 2 bytes
self.root_dispersion = (unpacked_data[5] >> 16) +
(unpacked_data[5] & 0xFFFF) / 2 ** 16
# 4 bytes
self.ref_id = str((unpacked_data[6] >> 24) & 0xFF) + " " +
str((unpacked_data[6] >> 16) & 0xFF) + " " +
str((unpacked_data[6] >> 8) & 0xFF) + " " +
str(unpacked_data[6] & 0xFF)
self.reference = unpacked_data[7] + unpacked_data[8] / 2 ** 32 # 8 bytes
self.originate = unpacked_data[9] + unpacked_data[10] / 2 ** 32 # 8 bytes
self.receive = unpacked_data[11] + unpacked_data[12] / 2 ** 32 # 8 bytes
self.transmit = unpacked_data[13] + unpacked_data[14] / 2 ** 32 # 8 bytes
return self
对于懒人来说,作为一个应用程序——将包变成漂亮字符串的代码
def to_display(self):
return "Leap indicator: {0.leap_indicator}n"
"Version number: {0.version_number}n"
"Mode: {0.mode}n"
"Stratum: {0.stratum}n"
"Pool: {0.pool}n"
"Precision: {0.precision}n"
"Root delay: {0.root_delay}n"
"Root dispersion: {0.root_dispersion}n"
"Ref id: {0.ref_id}n"
"Reference: {0.reference}n"
"Originate: {0.originate}n"
"Receive: {0.receive}n"
"Transmit: {0.transmit}"
.format(self)
发送包到服务器
向服务器发送包含填充字段的数据包 版本, 时尚 и 发送。 在 发送 您必须指定本地计算机上的当前时间(自 1 年 1900 月 1 日以来的秒数)、版本 - 4-3 中的任意一个、模式 - XNUMX(客户端模式)。
服务器收到请求后,填写 NTP 数据包中的所有字段,并将其复制到该字段中 起源 价值来自 发送,它出现在请求中。 我很困惑为什么客户不能立即填写他在现场的时间价值 起源。 结果,当数据包返回时,客户端有4个时间值——发送请求的时间(起源),服务器收到请求的时间(收集现金),服务器发送响应的时间(发送) 以及客户收到回复的时间 - 到达 (不在包装中)。 有了这些值我们就可以设置正确的时间。
包裹发送和接收代码
# Time difference between 1970 and 1900, seconds
FORMAT_DIFF = (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 3600
# Waiting time for recv (seconds)
WAITING_TIME = 5
server = "pool.ntp.org"
port = 123
packet = NTPPacket(version_number=2, mode=3, transmit=time.time() + FORMAT_DIFF)
answer = NTPPacket()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(WAITING_TIME)
s.sendto(packet.pack(), (server, port))
data = s.recv(48)
arrive_time = time.time() + FORMAT_DIFF
answer.unpack(data)
来自服务器的数据处理
服务器对数据的处理类似于 Raymond M. Smallian (1978) 的老问题中英国绅士的行为:“一个人没有手表,但家里有一个准确的挂钟,他可以用它来记录时间。”有时忘记了风。 有一天,他忘记重新启动时钟,于是去看望他的朋友,和他一起度过了一个晚上,当他回到家时,他成功地把时钟调对了。 如果事先不知道旅行时间,他是如何做到这一点的呢? 答案是:“离开家后,一个人给钟上发条并记住指针的位置。 来到朋友身边,离开客人,他记下到达和离开的时间。 这让他能够知道自己离开了多长时间。 一个人回到家,看着时钟,确定自己缺席的时间。 从这个时间中减去他访问的时间,该人就可以得出往返路上所花费的时间。 通过将路上花费的一半时间加上离开客人的时间,他就有机会找出到家的时间,并相应地调整时钟的指针。
查找服务器处理请求的时间:
- 查找数据包从客户端到服务器的传输时间: ((到达 - 始发) - (发送 - 接收)) / 2
- 找出客户端和服务器时间之间的差异:
接收 - 始发 - ((到达 - 始发) - (发送 - 接收)) / 2 =
2 * 接收 - 2 * 始发 - 到达 + 始发 + 传输 - 接收 =
接收 - 发起 - 到达 + 传输
我们将收到的价值添加到当地时间并享受生活。
结果输出
time_different = answer.get_time_different(arrive_time)
result = "Time difference: {}nServer time: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)