你好,habrayuzery。 今天,我想谈谈如何编写简单的NTP客户端。 基本上,我们将讨论数据包的结构以及如何处理来自NTP服务器的响应。 该代码将用python编写,因为在我看来,根本找不到用于此类事情的最佳语言。 鉴赏家会注意该代码与ntplib代码的相似性-我被它“启发”了。
那么NTP到底是什么? NTP-与精确时间服务器交互的协议。 许多现代机器都使用该协议。
例如,Windows中的w32tm服务。共有5个版本的NTP协议。 当前认为第一个第0版(1985,RFC958)已过时。 现在使用的是较新的版本,第一(1988,RFC1059),第二(1989,RFC1119),第三(1992,RFC1305)和第四(1996,RFC2030)。 1-4版本相互兼容,它们仅在服务器操作算法上有所不同。
包装格式
飞跃指示器 (校正指示器)-一个数字,显示有关第二次协调的警告。 值:
- 0-不更正
- 1-一天的最后一分钟包含61秒
- 2-一天的最后一分钟包含59秒
- 3-服务器故障(时间未同步)
版本号 -NTP协议的版本号(1-4)。
模式 -数据包发送方的操作模式。 值从0到7,最常见:
层 (分层级别)-服务器与参考时钟之间的中间层数(1-服务器直接从参考时钟获取数据,2-服务器从级别1的服务器获取数据,依此类推)。
轮询是一个有符号整数,表示连续消息之间的最大间隔。 此处,NTP客户端指示期望轮询服务器的时间间隔,而NTP服务器指示期望轮询服务器的时间间隔。 该值是秒的二进制对数。
精度是一个有符号整数,表示系统时钟的精度。 该值是秒的二进制对数。
根延迟 (服务器延迟)-时钟到达NTP服务器的时间,以秒为单位,以固定点为单位。
根离散度-NTP服务器时钟的离散度,以秒为单位,以固定点为单位。
Ref ID (来源标识符)-手表的ID。 如果服务器的层数为1,则ref id是原子钟的名称(4个ASCII字符)。 如果该服务器使用另一台服务器,则该服务器的地址将写在ref id中。
最后4个字段是时间-32位-整数部分,32位-小数部分。
参考 -服务器上的最新时钟。
始发 -发送数据包的时间(由服务器填充-下文中有更多说明)。
接收 -服务器接收到数据包的时间。
发送 -数据包从服务器发送到客户端的时间(由客户端填充,更多信息如下)。
我们将不考虑最后两个字段。
让我们来写我们的包:
包装代码class NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0):
要向服务器发送(和接收)数据包,我们必须能够将其转换为字节数组。
对于此(和反向)操作,我们将编写两个函数-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))
要选择要写入程序包的数字的小数部分,我们需要get_fraction()函数:
get_fraction() def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
开箱功能 def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6
对于懒惰的人,作为应用程序-将包变成漂亮的字符串的代码 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)
发送数据包到服务器
必须使用填充的
Version ,
Mode和
Transmit字段将数据包发送到服务器。 在“
传输”中,您需要指定本地计算机上的当前时间(自1900年1月1日以来的秒数),版本-1-4,模式-3(客户端模式)中的任何一种。
接受请求后,服务器通过将
Transmit值从请求复制到
Originate字段来填充NTP数据包中的所有字段。 对我来说,为什么客户不能立即在“
原始”字段中填写其时间值是一个谜。 结果,当数据包返回时,客户端有4次-发送请求的时间(
始发 ),服务器接收请求的时间(
Receive ),服务器发送响应的时间(
Transmit )和客户端接收响应的时间-
到达 (不在数据包中)。 使用这些值,我们可以设置正确的时间。
处理来自服务器的数据
处理服务器中的数据类似于英国绅士雷蒙德·M·苏利安(Raymond M. Sullian,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)
有用的
链接 。