Üdvözlet, Habr-felhasználók. Ma arról szeretnék beszélni, hogyan írhatunk egy egyszerű NTP klienst. Főként a csomag struktúrájáról és az NTP szerver válaszának feldolgozásáról lesz szó. A kód Python nyelven fog készülni, mert úgy érzem, ennél jobb nyelvet nem találunk az ilyen dolgokhoz. A szakértők észre fogják venni a kód hasonlóságát az ntplib kódjával — éppen ez inspirált.
Tehát mi is az az NTP? Az NTP – a pontos idővel rendelkező szerverekkel való kommunikációs protokoll. Ezt a protokollt számos modern gép használja. Például, a w32tm szolgáltatás Windows alatt.
Összesen 5 verzió létezik az NTP protokollból. Az első, 0-ás verzió (1985, RFC958) már elavultnak számít. Jelenleg újabb verziókat használnak, az 1-es (1988, RFC1059), a 2-es (1989, RFC1119), a 3-as (1992, RFC1305) és a 4-es (1996, RFC2030). Az 1-4 verziók kompatibilisek egymással, csak a működési algoritmusokban térnek el. szerverek.
Csomag formátuma

Leap indicator (korrekciós jelző) — egy szám, amely figyelmeztet a koherens másodpercre. Érték:
- 0 – nincs korrekció
- 1 – a nap utolsó perce 61 másodpercből áll
- 2 – a nap utolsó perce 59 másodpercből áll
- 3 – a szerver meghibásodása (az idő nincs szinkronizálva)
Version number (verziószám) – az NTP protokoll verziója (1-4).
Mode (mód) — a csomag küldőjének működési módja. Érték 0-tól 7-ig, a leggyakoribbak:
- 3 – kliens
- 4 – szerver
- 5 – továbbító mód
Stratum (réteg) – a rétegek száma a szerver és a referenciaórák között (1 – a szerver közvetlenül a referenciaóráktól nyeri az adatokat, 2 – a szerver az 1-es szintű szervertől kap adatokat, stb.).
Pool — egy aláírt egész szám, amely a maximális időtartamot jelzi a sorozatos üzenetek között. Az NTP-kliens itt azt az időtartamot jelöli, amellyel a szervert szeretné lekérdezni, míg az NTP-szerver azt az időtartamot, amellyel szeretné, hogy őt lekérdezzék. Az érték a másodpercek bináris logaritmusának felel meg.
Precision (pontosság) — egy aláírt egész szám, amely a rendszerek óráinak pontosságát jelzi. Az érték a másodpercek bináris logaritmusának felel meg.
Root delay (szerver késleltetés) – az az idő, amennyi idő alatt az órák adatai eljutnak az NTP-szerverhez, fixpontos másodpercben kifejezve.
Root dispersion (szerver mérési szórás) — az NTP-szerver óráinak adatai közötti szórás, fixpontos másodpercben kifejezve.
Ref id (forrás azonosító) – az óra azonosítója. Ha a szerver stratum 1-el rendelkezik, akkor a ref id – az atomórák neve (4 ASCII karakter). Ha a szerver egy másik szervert használ, akkor a ref id tartalmazza ennek a szervernek az címét.
Az utolsó 4 mező az időt képviseli – 32 bit – egész rész, 32 bit – törtrész.
Referens — az óra utolsó mérései a szerveren.
Eredet – az idő, amikor a csomag elküldésre került (a szerver tölti ki – erről később).
Fogadás – a csomag átvételi ideje a szerver által.
Küldés – a csomag elküldésének ideje a szervertől az ügyfélhez (az ügyfél tölti ki, erről is később).
A két utolsó mezőt nem vizsgáljuk meg.
Írjuk meg a csomagunkat:
Csomag kód
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Szükséges a szökőmásodperc bejegyzéséhez (2 bit)
self.leap_indicator = 0
# A protokoll verziója (3 bit)
self.version_number = version_number
# A küldő módja (3 bit)
self.mode = mode
# Az időtartam "rétegződésének" szintje (1 byte)
self.stratum = 0
# Időköz a lekérések között (1 byte)
self.pool = 0
# Pontosság (log2) (1 byte)
self.precision = 0
# Az óra elérésének időköze az NTP szerverig (4 byte)
self.root_delay = 0
# Az NTP-szerver óra szétszórása (4 byte)
self.root_dispersion = 0
# Az órák mutatója (4 byte)
self.ref_id = 0
# Az utolsó frissítés ideje a szerveren (8 byte)
self.reference = 0
# A csomag küldésének ideje a helyi gépről (8 byte)
self.originate = 0
# A szerver átvételi ideje (8 byte)
self.receive = 0
# A válasz küldésének ideje a szervertől (8 byte)
self.transmit = transmit
Ahhoz, hogy csomagot küldjünk (és fogadjunk) a szerverre, képesnek kell lennünk átalakítani azt bájt tömbbé.
Ehhez (és a fordított) művelethez két funkciót írunk — pack() és unpack():
A pack funkció
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))
Az unpack funkció
def unpack(self, data: bytes):
unpacked_data = struct.unpack(NTPPacket._FORMAT, data)
self.leap_indicator = unpacked_data[0] >> 6 # 2 bit
self.version_number = unpacked_data[0] >> 3 & 0b111 # 3 bit
self.mode = unpacked_data[0] & 0b111 # 3 bit
self.stratum = unpacked_data[1] # 1 byte
self.pool = unpacked_data[2] # 1 byte
self.precision = unpacked_data[3] # 1 byte
# 2 byte | 2 byte
self.root_delay = (unpacked_data[4] >> 16) +
(unpacked_data[4] & 0xFFFF) / 2 ** 16
# 2 byte | 2 byte
self.root_dispersion = (unpacked_data[5] >> 16) +
(unpacked_data[5] & 0xFFFF) / 2 ** 16
# 4 byte
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 byte
self.originate = unpacked_data[9] + unpacked_data[10] / 2 ** 32 # 8 byte
self.receive = unpacked_data[11] + unpacked_data[12] / 2 ** 32 # 8 byte
self.transmit = unpacked_data[13] + unpacked_data[14] / 2 ** 32 # 8 byte
return self
A lustálók számára, itt a kód, amely a csomagot egy szép szöveggé alakítja
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)
Csomag küldése a szervernek
A szervernek egy kitöltött mezőkkel rendelkező csomagot kell küldeni Verzió, Mode és Küldés. A Küldés meg kell adni a helyi gép aktuális idejét (másodpercek száma 1900. január 1. óta), a verziónak - bármely 1-4 közötti számnak, a módnak - 3-nak (kliense üzemmód).
A szerver, aki megkapja a kérdést, kitölt minden mezőt az NTP-csomagban, átmásolva a mezőbe Eredet az értéket Küldés, amely a kérésben érkezett. Számomra rejtély, miért nem tudja a kliens azonnal kitölteni a saját idejét a mezőbe Eredet. Végül, amikor a csomag visszaérkezik, a kliensnek 4 időértéke van - a kérés elküldésének ideje (Eredet), a szerver általi kérés fogadásának ideje (Fogadás), a szerver általi válasz elküldésének ideje (Küldés) és a kliens általi válasz fogadásának ideje - Érkezik (nincs a csomagban). Ezekkel az értékekkel meg tudjuk határozni a helyes időt.
A csomag küldésének és fogadásának kódja
# 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)
Adatok feldolgozása a szerverből
A szerverből származó adatok feldolgozása hasonló ahhoz, amit Raymond M. Smullyan (1978) régi feladata egy angol úriemberről mond. "Egy embernek nem volt karórája, viszont a falon pontos falióra lógott, amit néha elfelejtett beállítani. Egy alkalommal, mikor megint nem állította be az órát, elment egy barátjához, eltöltött egy estét nála, majd hazatérve sikerült az óráját helyesen beállítania. Hogyan sikerült ezt tennie, ha az út idejét előre nem tudta?" A válasz: "Mikor elhagyta otthonát, az ember beállította az órát és megjegyezte, hogy a mutatók milyen helyzetben vannak. Mikor a barátjához ment és távozott, feljegyezte, mikor érkezett és hagyta el a házat. Ez lehetővé tette számára, hogy megtudja, mennyi időt töltött a barátjánál. Mikor hazatérve rápillantott az órájára, meg tudta határozni, mennyi ideig volt távol. Ebből az időből levonva azt az időt, amit a barátjánál töltött, megtudta, mennyi időt vett igénybe az oda- és visszaút. Hozzáadva a barátjától való távozás idejéhez a megtett idő felét, képes volt megtudni, mikor érkezett haza és így beállítani az órája mutatóit megfelelően.",
Keressük meg a szerver munka idejét a kérés feldolgozása során:
- Keressük meg a csomag útvonalának idejét az ügyféltől a szerverig: ((Érkezik – Eredeti) — (Átviteli – Fogadott)) / 2
- Keressük meg a különbséget az ügyfél és a szerver ideje között:
Fogadott — Eredeti — ((Érkezik – Eredeti) — (Átviteli – Fogadott)) / 2 =
2 * Fogadott — 2 * Eredeti – Érkezik + Eredeti + Átviteli – Fogadott =
Fogadott – Eredeti – Érkezik + Átviteli
Adjuk hozzá a kapott értéket a helyi időhöz, és örüljünk az életnek.
Eredmény kiírása
time_different = answer.get_time_different(arrive_time)
result = "Időeltérés: {}nSzerver idő: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Hasznos .
Forrás: habr.com
