你好,哈布拉用戶。 今天我想談談如何編寫自己的簡單的 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 彼此兼容,它們的區別僅在於服務器的算法。
數據包格式
跳躍指示器 (校正指示器)是指示閏秒警告的數字。 意義:
- 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) 的老問題中英國紳士的行為:“一個人沒有手錶,但家裡有一個準確的掛鐘,他可以用它來記錄時間。”有時忘記了風。 有一天,他忘記重新啟動時鐘,於是去看望他的朋友,和他一起度過了一個晚上,當他回到家時,他成功地把時鐘調對了。 如果事先不知道旅行時間,他是如何做到這一點的呢? 答案是:“離開家後,一個人給鐘上發條並記住指針的位置。 來到朋友身邊,離開客人,他記下到達和離開的時間。 這讓他能夠知道自己離開了多長時間。 一個人回到家,看著時鐘,確定自己缺席的時間。 從這個時間中減去他訪問的時間,該人就可以得出往返路上所花費的時間。 通過將路上花費的一半時間加上離開客人的時間,他就有機會找出到家的時間,並相應地調整時鐘的指針。
查找服務器處理請求的時間:
- 查找數據包從客戶端到服務器的傳輸時間: ((到達 - 始發) - (發送 - 接收)) / 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