编写一个简单的 NTP 客户端

你好,哈布拉用户。 今天我想谈谈如何编写自己的简单的 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 彼此兼容,它们的区别仅在于服务器的算法。

数据包格式

编写一个简单的 NTP 客户端

跳跃指示器 (校正指示器)是指示闰秒警告的数字。 意义:

  • 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) 的老问题中英国绅士的行为:“一个人没有手表,但家里有一个准确的挂钟,他可以用它来记录时间。”有时忘记了风。 有一天,他忘记重新启动时钟,于是去看望他的朋友,和他一起度过了一个晚上,当他回到家时,他成功地把时钟调对了。 如果事先不知道旅行时间,他是如何做到这一点的呢? 答案是:“离开家后,一个人给钟上发条并记住指针的位置。 来到朋友身边,离开客人,他记下到达和离开的时间。 这让他能够知道自己离开了多长时间。 一个人回到家,看着时钟,确定自己缺席的时间。 从这个时间中减去他访问的时间,该人就可以得出往返路上所花费的时间。 通过将路上花费的一半时间加上离开客人的时间,他就有机会找出到家的时间,并相应地调整时钟的指针。

查找服务器处理请求的时间:

  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)

有用 链接.

来源: habr.com

添加评论