編寫一個簡單的 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)

有用 鏈接.

來源: www.habr.com

添加評論