编写一个简单的NTP客户端

你好,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,最常见:

  • 3-客户
  • 4-服务器
  • 5-广播模式

(分层级别)-服务器与参考时钟之间的中间层数(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): # 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)) 


要选择要写入程序包的数字的小数部分,我们需要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 # 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) 


发送数据包到服务器


必须使用填充的VersionModeTransmit字段将数据包发送到服务器。 在“ 传输”中,您需要指定本地计算机上的当前时间(自1900年1月1日以来的秒数),版本-1-4,模式-3(客户端模式)中的任何一种。

接受请求后,服务器通过将Transmit值从请求复制到Originate字段来填充NTP数据包中的所有字段。 对我来说,为什么客户不能立即在“ 原始”字段中填写其时间值是一个谜。 结果,当数据包返回时,客户端有4次-发送请求的时间( 始发 ),服务器接收请求的时间( Receive ),服务器发送响应的时间( Transmit )和客户端接收响应的时间- 到达 (不在数据包中)。 使用这些值,我们可以设置正确的时间。

包裹收发代码
 # 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) 


处理来自服务器的数据


处理服务器中的数据类似于英国绅士雷蒙德·M·苏利安(Raymond M. Sullian,1978)的旧任务中的动作:“一个人没有手表,但另一方面,他有一个确切的挂钟,他有时忘记了启动。 有一次,他忘了重新开始看表,他去拜访他的朋友,在那个地方度过了一个晚上,当他回到家时,他设法正确地设定了时钟。 如果提前不知道旅行时间,他如何做到这一点?” 答案是:“一个人离开家时,手表会启动,并记住手的位置。 他来到朋友身边并离开客人时,记下了他到达和离开的时间。 这样一来,他就可以了解到他正在访问多少。 一个人回到家看了看表,就确定了他缺席的时间。 从这个时间中减去他在一次访问中花费的时间,一个人就会知道来回旅行所花费的时间。 他将旅途花费的时间增加了一半,使他有机会找出到达家的时间,并相应地翻译钟针。”

我们根据请求找到服务器的工作时间:

  1. 我们找到从客户端到服务器的数据包路径时间: ((到达-发起)-(发送-接收))/ 2
  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) 


有用的链接

Source: https://habr.com/ru/post/zh-CN448060/


All Articles