编写一个简单的 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

添加评论