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

添加評論