Tere, hÀÀli kasutajad. TÀna tahan rÀÀkida sellest, kuidas kirjutada oma lihtne NTP klient. Peamiselt rÀÀgime paketi struktuurist ja sellest, kuidas töödelda vastust NTP serverilt. Kood kirjutatakse Pythoni keeles, sest nagu mulle tundub, ei leidu paremat keelt selliste asjaolude jaoks.
Nii, mis asi see NTP siis on? NTP â protokoll, mille kaudu suheldakse tĂ€psusaja serveritega. Seda protokolli kasutatakse paljudes kaasaegsetes seadmetes. NĂ€iteks w32tm teenus Windowsis.
Kokku on olemas 5 NTP protokolli versiooni. Esimene, nullandmeversioon (1985, RFC958), loetakse praegu vananenuks. Praegu kasutatakse uusi versioone: esimene (1988, RFC1059), teine (1989, RFC1119), kolmas (1992, RFC1305) ja neljas (1996, RFC2030). Esimene kuni neljas versioon on omavahel ĂŒhilduvad, need erinevad vaid töö algoritmide poolest. serverite.
Paketi formaat

Leap indicator (korrektsiooninĂ€idik) â number, mis nĂ€itab hoiatussekundi koordinaate. VÀÀrtus:
- 0 â ei ole korrektsiooni
- 1 â pĂ€eva viimane minut sisaldab 61 sekundi
- 2 â pĂ€eva viimane minut sisaldab 59 sekundi
- 3 â server fault (time not synchronized)
Versiooninumber (versiooninumber) â NTP-protokollan versioonin numero (1-4).
ReĆŸiim (reĆŸiim) â paketti saatavan lĂ€hettĂ€jĂ€n toimintatila. Arvo 0-7, yleisimmĂ€t:
- 3 â asiakas
- 4 â palvelin
- 5 â laajennusreĆŸiimi
Stratum (kerros) â vĂ€likerrosten mÀÀrĂ€ palvelimen ja vertailuaikojen vĂ€lillĂ€ (1 â palvelin ottaa tiedot suoraan vertailuaikasilta, 2 â palvelin ottaa tiedot palvelimelta, jonka kerros on 1 jne.).
Pool â merkkiluku, joka edustaa suurinta vĂ€liĂ€ perĂ€kkĂ€isten viestien vĂ€lillĂ€. NTP-asiakas mÀÀrittelee tĂ€ssĂ€ vĂ€lin, jolla se aikoo kysyĂ€ palvelinta, ja NTP-palvelin â vĂ€lin, jolla se odottaa kysyttĂ€vĂ€n. Arvo on sekuntien binaarilukujohdin.
Tarkkuus (tarkkuus) â merkkiluku, joka edustaa jĂ€rjestelmĂ€kellojen tarkkuutta. Arvo on sekuntien binaarilukujohdin.
Root-viive (palvelimen viive) â aika, jonka kuluessa kellojen nĂ€yttö saapuu NTP-palvelimelle, sekunteina kiinteĂ€llĂ€ pisteellĂ€.
Root-hajonta (serveri nĂ€idud vahe) â NTP-serveri aja nĂ€itude vahe sekundites, fikseeritud punktiga.
Ref id (allika identifikaator) â kellade id. Kui serveril on stratum 1, siis ref id â aatomikellade nimetus (4 ASCII tĂ€hte). Kui server kasutab teist serverit, siis ref id-s on selle serveri aadress.
Viimased 4 vĂ€ljad esindavad aega â 32 bitti â tervikosa, 32 bitti â murdosa.
Reference â viimased kella nĂ€idud serveris.
Originate â aeg, mil pakett saadeti (tĂ€idetakse serveri poolt â sellest rÀÀgime allpool).
Receive â aeg, mil server sai paketti.
Transmit â aeg, mil server saadab paketti kliendile (tĂ€idetakse kliendi poolt, sellest samuti allpool).
Kahte viimast vÀljad 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):
# Ăksikasjalik teave hĂŒppe sekundi kohta (2 bitti)
self.leap_indicator = 0
# Protokolli versioon (3 bitti)
self.version_number = version_number
# Saatja reĆŸiim (3 bitti)
self.mode = mode
# "Kihilisuse" lugemise tase (1 bait)
self.stratum = 0
# Aja pÀringu vahe (1 bait)
self.pool = 0
# TĂ€psus (log2) (1 bait)
self.precision = 0
# Kella jÔudmise vahe NTP serverisse (4 baiti)
self.root_delay = 0
# Kellade hajumine NTP-serveris (4 baiti)
self.root_dispersion = 0
# Kellade indikaator (4 baiti)
self.ref_id = 0
# Viimase uuenduse aeg serveris (8 baiti)
self.reference = 0
# Paketi saatmise aeg kohalikust masinast (8 baiti)
self.originate = 0
# VastuvÔtu aeg serveris (8 baiti)
self.receive = 0
# Vastuse saatmise aeg serverist (8 baiti)
self.transmit = transmit
Et saata (ja vastu vÔtta) pakette serverile, peame oskama neid muuta baitide massiiviks.
Selle (ja tagasikĂ€igu) operatsiooni 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))
Unpack funktsioon
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, lisana â 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 saatjale saatmine
Serverile tuleb saata paket, mille vĂ€ljad on tĂ€idetud Versioon, ReĆŸiim ja Transmit. Dokumendihalduses Transmit on vajalik mÀÀrata kohalikul masinal praegune aeg (sekundid alates 1. jaanuarist 1900), versioon â ĂŒkskĂ”ik milline number vahemikus 1-4, reĆŸiim â 3 (klientide reĆŸiim).
Server, mis on pĂ€ringu vastu vĂ”tnud, tĂ€idab NTP-paketis kĂ”ik vĂ€ljad, kopeerides vĂ€ljal Originate vÀÀrtuse Transmit, mis tuli pĂ€ringus. On mĂŒsteerium, miks klient ei saa kohe oma aega vĂ€lja tĂ€ita. Originate. LĂ”puks, kui pakett tagasi jĂ”uab, on kliendil 4 ajavÀÀrtust â pĂ€ringu saatmise aeg (Originate), serveri pĂ€ringu vastuvĂ”tmise aeg (Receive), serveri vastuse saatmise aeg (Transmit) ja vastuse vastuvĂ”tmise aeg kliendi poolt â Saabumine (paketis ei ole). Nende vÀÀrtuste abil saame mÀÀrata Ă”ige aja.
Paketi saatmine ja vastuvÔtmine
# 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 sarnaneb inglise mehe tegevustega Raymond M. Smullyani vanast ĂŒlesandest (1978): "Ăhel mehel ei olnud randme kellasid, kuid kodus olid tĂ€psed seinakellad, mida ta mĂ”nikord unustas ĂŒles kerida. Ăkskord, unustades jĂ€lle kella ĂŒles kerida, lĂ€ks ta sĂ”bra kĂŒlla, veetis seal Ă”htu ja naastes koju, suutis ta kella Ă”igesti seadistada. Kuidas tal see Ă”nnestus, kui reisi aeg polnud ette teada?" Vastus on jĂ€rgmine: "Kodust lahkudes keerab mees kella ĂŒles ja mĂ€letab, millises asendis on kĂ€ed. KĂŒlla jĂ”udes ja seal olles mĂ€rkab ta, millal ta tuli ja lahkus. See aitab tal teada, kui kaua ta kĂŒlas viibis. Kodus tagasi ja kella vaadates mÀÀrab mees oma puudumise kestuse. Lahutades sellest ajast selle, mille ta veetis kĂŒlaskĂ€igus, saab ta teada, kui kaua kulus teekonnale kokku. Lisades kĂŒlaskĂ€igust lahkumise ajale poole tee lĂ€bimise aega, suudab ta kindlaks mÀÀrata kodus saabumise aja ja vastavalt sellele oma kella kĂ€ed seadistada."
Otsime serveri tööaega pÀringu osas:
- Otsime paketi teekonna aega kliendist serverisse: ((Saabumine â Algus) â (Edastamine â VastuvĂ”tt)) / 2
- Otsime erinevust kliendi ja serveri vahel:
VastuvĂ”tt â Algus â ((Saabumine â Algus) â (Edastamine â VastuvĂ”tt)) / 2 =
2 * VastuvĂ”tt â 2 * Algus â Saabumine + Algus + Edastamine â VastuvĂ”tt =
VastuvĂ”tt â Algus â Saabumine + Edastamine
Lisame saadud vÀÀrtuse kohalikule ajale ja naudime elu.
Tulemuse vÀljund
time_different = answer.get_time_different(arrive_time)
result = "Ajavahe: {}nServeri aeg: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Kasulik .
Allikas: habr.com
