Tere, Habr kasutajad. TĂ€na tahan rÀÀkida, kuidas kirjutada oma lihtne NTP klient. Peamiselt rÀÀgime paketi struktuurist ja vastuse töötlemise viisist NTP serverilt. Kood kirjutatakse Pythonis, sest minu arvates ei leia selliste asjade jaoks paremat keelt. Teadjad panevad tĂ€hele, et kood sarnaneb ntplib koodiga â ma olen just sellest "inspireeritud".
Nii, mis asi on NTP? NTP on protokoll, mis suhtleb tÀpsusaja serveritega. Seda protokolli kasutatakse paljudes kaasaegsetes masinates. NÀiteks, teenus w32tm Windowsis.
Kokku on olemas 5 NTP protokolli versiooni. Esimene, 0. versioon (1985, RFC958), loetakse praegu aegunuks. Praegu kasutatakse uuemaid versioone: 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) ja 4. (1996, RFC2030). Versioonid 1-4 on omavahel ĂŒhilduvad, need erinevad vaid tööalgoritmide poolest. serverid.
Paketi formaat

Leap indicator (korrektsiooni indikaator) â number, mis nĂ€itab koordineeritud tunni hoiatust. VÀÀrtus:
- 0 â korrektsiooni ei ole
- 1 â pĂ€eva viimane minut sisaldab 61 sekundit
- 2 â pĂ€eva viimane minut sisaldab 59 sekundit
- 3 â serveri rike (aeg ei ole sĂŒnkroniseeritud)
Version number (versiooninumber) â NTP protokolli versiooninumber (1-4).
Mode (reĆŸiim) â paketi saatja tööreĆŸiim. VÀÀrtused 0 kuni 7, kĂ”ige sagedasemad:
- 3 â klient
- 4 â server
- 5 â laialdane reĆŸiim
Stratum (kihistumise tase) â kihi vahemaa arv serveri ja viidatud kellade vahel (1 â server vĂ”tab andmeid vahetult viidatud kelladelt, 2 â server vĂ”tab andmeid 1. taseme serverilt jne).
Pool â mĂ€rgitud tĂ€isarv, mis esindab maksimaalset intervalli jĂ€rjestikuste sĂ”numite vahel. NTP klient mÀÀrab siin intervalli, millest ta eeldab serveri kĂŒsitlemist, ning NTP server mÀÀrab intervalli, milles ta eeldab, et teda kĂŒsitakse. VÀÀrtus on vĂ”rdne sekundite binaarse logaritmi vÀÀrtusega.
Precision (tĂ€psus) â mĂ€rgitud tĂ€isarv, mis esindab sĂŒsteemi kellade tĂ€psust. VÀÀrtus on sekundite binaarse logaritmi vÀÀrtus.
Root delay (serveri viivitus) â aeg, mille jooksul kellade nĂ€idud jĂ”uavad NTP serverisse, fixed-point sekundi arvuna.
Root dispersion (serveri nĂ€itude hajumine) â NTP serveri kellade nĂ€itude hajumine fixed-point sekundi arvuna.
Ref id (allika identifikaator) â id kella. Kui serveril on stratum 1, siis ref id â aatomkellade nimi (4 ASCII mĂ€rki). Kui server kasutab teist serverit, siis ref id-s on selles serveris asukoht.
Viimased 4 vĂ€ljad tĂ€hendavad aega â 32 bitti â tĂ€isarv, 32 bitti â murdarv.
Viitega â serveri viimased kella nĂ€idud.
Algus â aeg, mil pakett saadeti (tĂ€idetakse serveri poolt â sellest allpool).
Saada â aeg, mil server pakketi sai.
Edastamine â aeg, mil pakett serverist kliendile saadeti (tĂ€idetakse kliendi poolt, sellest ka allpool).
Kahte viimast vÀlja me ei kÀsitle.
Kirjutame meie paketi:
Paketi kood
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Vajalik leap second (2 bitti)
self.leap_indicator = 0
# Protokolli versioon (3 bitti)
self.version_number = version_number
# Saaja reĆŸiim (3 bitti)
self.mode = mode
# Kihi âkihtimiseâ taseme lugemine (1 bait)
self.stratum = 0
# Taotluste vahe (1 bait)
self.pool = 0
# TĂ€psus (log2) (1 bait)
self.precision = 0
# Aeg, mis on vajalik NTP serveri juurde jÔudmiseks (4 baiti)
self.root_delay = 0
# NTP-serveri kella hajutamine (4 baiti)
self.root_dispersion = 0
# Kellade indikaator (4 baiti)
self.ref_id = 0
# Viimane uuendamise aeg serveris (8 baiti)
self.reference = 0
# Aeg, mil pakett saadetakse kohalikult masinalt (8 baiti)
self.originate = 0
# Aeg, mil server paketi sai (8 baiti)
self.receive = 0
# Aeg, mil vastus serverist saadeti (8 baiti)
self.transmit = transmit
Pakketi saatmiseks (ja vastuvÔtmiseks) peame oskama seda massiiviks transformeerida.
Selle (ja tagasitee) toimingu jaoks kirjutame kaks funktsiooni â pack() ja unpack():
Funktsioon 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))
Funktsioon 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
Laiskade jaoks, rakenduse vormis â kood, mis muudab paketi ilusaks stringiks
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)
Paketi saatmine serverisse
Serverisse tuleb saata pakett, mille vĂ€ljad on tĂ€idetud Version, Mode ja Edastamine. Failis Edastamine on vajalik mĂ€rkida kohaliku masina praegune kellaaeg (sekundite arv alates 1. jaanuarist 1900), versioon â ĂŒkskĂ”ik milline vahemik 1-4, reĆŸiim â 3 (kliendi reĆŸiim).
Server, kes saab nĂ”ude, tĂ€idab NTP-paketis kĂ”ik vĂ€ljad, kopeerides vĂ€lja Algus vaĂ€rtuse, mis tuli Edastamine, mis saabus nĂ”udes. Minu jaoks on mĂ”istatus, miks klient ei saa kohe tĂ€ita oma aja vÀÀrtust vĂ€ljale Algus. LĂ”ppkokkuvĂ”ttes, kui pakett tagasi tuleb, on kliendil 4 ajavÀÀrtust â nĂ”ude saatmise aeg (Algus), serveri poolt nĂ”ude saamise aeg (Saada), serveri poolt vastuse saatmise aeg (Edastamine) ja vastuse saabumise aeg kliendile â Saabub (paketis pole). Nende vÀÀrtuste abil saame mÀÀrata Ă”ige aja.
Paketist saatmise ja vastuvÔtmise kood
# 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)
Andmete töötlemine serverist
Andmete töötlemine serverist on sarnane inglise hĂ€rrasmehe tegudele Raymond M. Smullyani vanas ĂŒlesandes (1978): âĂhel inimesel ei olnud kĂ€ekella, kuid kodus riputas tĂ€pne seinakell, mille ta mĂ”nikord unustas maha keerata. Ăhel pĂ€eval, unustades jĂ€lle kella maha keerata, lĂ€ks ta sĂ”bra juurde kĂŒlla, veetis seal Ă”htu ja tagasi koju tulles suutis ta kella Ă”igesti seadistada. Kuidas ta seda tegi, kui tee pikkus ei olnud ette teada?â Vastus on: âVĂ€ljudes kodust, keerab inimene kella ĂŒles ja mĂ€letab, millises asendis on kĂ€ed. SĂ”bra juures olles ja kĂŒlast lahkudes mĂ€rkab ta oma saabumise ja lahkumise kellaaegu. See vĂ”imaldab tal teada, kui kaua ta kĂŒlas oli. Tagasi koju jĂ”udes ja kellale vaadates, mÀÀrab ta oma puudumise kestuse. Lahutades sellest ajast kĂŒlas oldud aja, saab ta teada, kui kaua kulus teekonnale edasi ja tagasi. Lisades lahkumise ajale poole teekonna kestusest, saab ta teada, millal koju jĂ”udis, ja saab oma kella vastavalt seadistada.â
Leidke serveri tööaeg pÀringu jaoks:
- Leidke paketi teekonna aeg kliendi ja serveri vahel: ((Saabumine â Algus) â (Edastus â VastuvĂ”tt)) / 2
- Leidke erinevus kliendi ja serveri aja vahel:
VastuvĂ”tt â Algus â ((Saabumine â Algus) â (Edastus â VastuvĂ”tt)) / 2 =
2 * VastuvĂ”tt â 2 * Algus â Saabumine + Algus + Edastus â VastuvĂ”tt =
VastuvĂ”tt â Algus â Saabumine + Edastus
Lisame saadud vÀÀrtuse kohalikku aega ja naudime elu.
Tulemuse vÀljastamine
time_different = answer.get_time_different(arrive_time)
result = "Aja erinevus: {}nServeri aeg: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Kasulik .
Allikas: habr.com
