Dobrý den, uživatelé Habr. Dnes bych vám chtěl říct, jak napsat svého jednoduchého NTP klienta. Hlavně se budu věnovat struktuře paketu a způsobu zpracování odpovědi z NTP serveru. Kód bude napsán v Pythonu, protože se domnívám, že lepší jazyk pro podobné úkoly prostě nenajdete. Odborníci si všimnou podobnosti kódu s kódem ntplib — tím jsem se právě inspiroval.
Takže, co vlastně NTP je? NTP je protokol pro interakci se servery přesného času. Tento protokol se používá na mnoha moderních strojích. Například služba w32tm ve Windows.
Existuje celkem 5 verzí NTP protokolu. První, 0. verze (1985, RFC958), je v současnosti považována za zastaralou. V současnosti se používají novější, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) a 4. (1996, RFC2030). Verze 1-4 jsou vzájemně kompatibilní, liší se pouze algoritmy práce. servery.
Formát paketu

Leap indicator (indikátor korekce) — číslo, které ukazuje varování o sekundě koordinace. Hodnota:
- 0 – žádná korekce
- 1 – poslední minuta dne obsahuje 61 sekund
- 2 – poslední minuta dne obsahuje 59 sekund
- 3 – porucha serveru (čas není synchronizován)
Version number (číslo verze) – číslo verze NTP protokolu (1-4).
Mode (režim) — režim práce odesílatele paketu. Hodnota od 0 do 7, nejběžnější:
- 3 – klient
- 4 – server
- 5 – režim vysílání
Stratum (úroveň vrstvy) – počet mezivrstvy mezi serverem a etalonovými hodinami (1 – server získává data přímo z etalonových hodin, 2 – server získává data ze serveru s úrovní 1 atd.).
Pool — celé číslo se znaménkem, představující maximální interval mezi po sobě jdoucími zprávami. NTP klient zde uvádí interval, s jakým se domnívá, že bude dotazovat server, a NTP server – interval, s jakým chce, aby byl dotazován. Hodnota je rovna dvojkovému logaritmu vteřin.
Precision (přesnost) — celé číslo se znaménkem, představující přesnost systémových hodin. Hodnota je rovna dvojkovému logaritmu vteřin.
Root delay (zpoždění serveru) – čas, za který údaje hodin dorazí k NTP serveru, jako počet vteřin s pevnou desetinnou čárkou.
Root dispersion (odchylka měření serveru) — odchylka měření hodin NTP serveru jako číslo vteřin s pevnou desetinnou čárkou.
Ref id (identifikátor zdroje) – id hodin. Pokud má server stratum 1, pak ref id – název atomových hodin (4 ASCII znaky). Pokud server používá jiný server, je v ref id uvedena adresa tohoto serveru.
Poslední 4 pole představují čas – 32 bitů – celá část, 32 bitů – zlomek.
Reference — poslední měření hodin na serveru.
Originate – čas, kdy byl paket odeslán (vyplňuje server – o tom více níže).
Receive – čas přijetí paketu serverem.
Transmit – čas odeslání paketu ze serveru klientovi (vyplňuje klient, o tom také níže).
Poslední dvě pole nebudeme zvažovat.
Napišme náš paket:
Kód paketu
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Potřebné pro zadání skokového sekundy (2 bity)
self.leap_indicator = 0
# Verze protokolu (3 bity)
self.version_number = version_number
# Mód odesílatele (3 bity)
self.mode = mode
# Úroveň "vrstvení" čtení času (1 byte)
self.stratum = 0
# Interval mezi žádostmi (1 byte)
self.pool = 0
# Přesnost (log2) (1 byte)
self.precision = 0
# Interval pro dosažení NTP serveru (4 bytes)
self.root_delay = 0
# Rozptýlení hodin NTP-serveru (4 bytes)
self.root_dispersion = 0
# Ukazatel hodin (4 bytes)
self.ref_id = 0
# Poslední aktualizační čas na serveru (8 bytes)
self.reference = 0
# Čas odeslání paketu z místního stroje (8 bytes)
self.originate = 0
# Čas přijetí na serveru (8 bytes)
self.receive = 0
# Čas odeslání odpovědi ze serveru (8 bytes)
self.transmit = transmit
Abychom mohli odesílat (a přijímat) paket na server, musíme umět převádět ho na pole bajtů.
Pro tuto (a zpětnou) operaci napíšeme dvě funkce – pack() a unpack():
Funkce pack
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))
Funkce unpack
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
Pro lenochy jako aplikace – kód, který převede paket na pěkný řetězec
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)
Odeslání paketu na server
Na server je třeba odeslat paket se vyplněnými poli Verze, Mode a Transmit. V Transmit je nutné uvést aktuální čas na lokálním počítači (počet sekund od 1. ledna 1900), verzi – jakoukoli z 1-4, režim – 3 (režim klienta).
Server, který přijme žádost, vyplní v NTP paketu všechna pole zkopírováním hodnoty do pole Originate získané z Transmit, které bylo obdrženo v žádosti. Zůstává mi záhadou, proč klient nemůže hned vyplnit hodnotu svého času do pole Originate. Nakonec, když paket dorazí zpět, má klient 4 časové hodnoty – čas odeslání žádosti (Originate), čas přijetí žádosti serverem (Receive), čas odeslání odpovědi serverem (Transmit) a čas přijetí odpovědi klientem – Dorazí (není v paketu). Pomocí těchto hodnot můžeme nastavit správný čas.
Kód odesílání a přijímání paketu
# 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)
Zpracování dat ze serveru
Zpracování dat ze serveru je podobné činům anglického gentlemana z úkolu Raymonda M. Smaliana (1978): „Jednomu muži scházely hodinky, ale doma měl přesné nástěnné hodiny, které občas zapomínal natahovat. Jednou, když opět zapomněl hodiny natočit, šel na návštěvu k příteli, strávil u něj večer a když se vrátil domů, dokázal správně nastavit hodiny. Jak mu to bylo možné, když čas cesty nebyl předem znám?“ Odpověď je následující: „Při odchodu z domu si muž natahuje hodiny a zapamatuje si, v jaké poloze jsou ručičky. Když dorazí k příteli a odchází od něj, zaznamenává si čas svého příchodu a odchodu. To mu umožňuje zjistit, kolik času u přítele strávil. Po návratu domů a pohledu na hodiny určuje délku své nepřítomnosti. K tomu času odečte dobu, kterou strávil u přítele, a zjistí tak čas strávený na cestě tam a zpět. Přidáním poloviny doby strávené na cestě k času odchodu od přítelkyně získává možnost zjistit čas příjezdu domů a správně nastavit ručičky svých hodin.“
Zjistíme dobu práce serveru nad požadavkem:
- Zjistíme dobu cesty paketu od klienta k serveru: ((Arrive – Originate) — (Transmit – Receive)) / 2
- Zjistíme rozdíl mezi časem klienta a serveru:
Receive — Originate — ((Arrive – Originate) — (Transmit – Receive)) / 2 =
2 * Receive — 2 * Originate – Arrive + Originate + Transmit – Receive =
Receive – Originate – Arrive + Transmit
Přidáme získanou hodnotu k místnímu času a raduje se ze života.
Výstup výsledku
time_different = answer.get_time_different(arrive_time)
result = "Časový rozdíl: {}nČas na serveru: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Užitečné .
Zdroj: habr.com
