Hallo Habr-gebruikers. Vandaag wil ik vertellen over hoe je je eigen eenvoudige NTP-client kunt schrijven. Het gesprek zal voornamelijk gaan over de pakketstructuur en de manier waarop het antwoord van de NTP-server wordt verwerkt. De code wordt geschreven in Python, omdat ik denk dat er geen betere taal voor dergelijke zaken te vinden is. Kenners zullen de overeenkomst tussen de code en die van ntplib opmerken - ik was er juist door geïnspireerd.
Dus, wat is NTP eigenlijk? NTP is een protocol voor interactie met tijdservers. Dit protocol wordt gebruikt in veel moderne machines. Bijvoorbeeld, de w32tm-service in Windows.
Er zijn in totaal 5 versies van het NTP-protocol. De eerste, 0-versie (1985, RFC958), wordt momenteel als verouderd beschouwd. Momenteel worden nieuwere versies gebruikt: 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) en 4 (1996, RFC2030). Versies 1-4 zijn onderling compatibel en verschillen alleen in hun werkalgoritmen. servers.
Pakketformaat

Leap indicator (correctie-indicator) - een getal dat een waarschuwing geeft voor een coördinatieseconde. Waarde:
- 0 - geen correctie
- 1 - de laatste minuut van de dag bevat 61 seconden
- 2 - de laatste minuut van de dag bevat 59 seconden
- 3 - serverfout (tijd niet gesynchroniseerd)
Versienummer (versienummer) - het versienummer van het NTP-protocol (1-4).
Modus (werkmodus) - de werkmodus van de verzender van het pakket. Waarde van 0 tot 7, de meest voorkomende:
- 3 - client
- 4 - server
- 5 - broadcastmodus
Stratum (naaldfactor) - het aantal tussenliggende lagen tussen de server en de referentieklokken (1 - de server haalt gegevens rechtstreeks van de referentieklokken, 2 - de server haalt gegevens van een server met niveau 1, enz.).
Pool - een geheel getal met teken, dat het maximale interval tussen opeenvolgende berichten voorstelt. De NTP-client geeft hier het interval aan waarmee hij de server wil ondervragen, en de NTP-server het interval waarmee hij verwacht te worden ondervraagd. De waarde is gelijk aan de binaire logaritme van seconden.
Precisie (nauwkeurigheid) - een geheel getal met teken, dat de nauwkeurigheid van de systeemklokken vertegenwoordigt. De waarde is gelijk aan de binaire logaritme van seconden.
Root delay (serververtraging) - de tijd die nodig is voor de klokmetingen om de NTP-server te bereiken, als het aantal seconden met een vast punt.
Root dispersion (variatie van servermetingen) - de variatie van de klokmetingen van de NTP-server als een aantal seconden met een vast punt.
Ref id (bron identificatie) – id van de klok. Als de server stratum 1 heeft, dan is ref id de naam van de atoomklok (4 ASCII-tekens). Als de server een andere server gebruikt, dan staat het adres van die server in ref id.
De laatste 4 velden vertegenwoordigen de tijd – 32 bits – een geheel getal, 32 bits – een fractie.
Referentie – de laatste metingen van de klok op de server.
Origineer – de tijd waarop het pakket werd verzonden (wordt door de server ingevuld – daarover hieronder meer).
Ontvang – de tijd waarop het pakket door de server wordt ontvangen.
Verzend – de tijd van verzending van het pakket van de server naar de client (wordt door de client ingevuld, daarover ook hieronder meer).
We zullen de laatste twee velden niet behandelen.
Laten we ons pakket schrijven:
Pakketcode
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Noodzakelijk om sprongetjes van seconden in te voeren (2 bits)
self.leap_indicator = 0
# Versie van het protocol (3 bits)
self.version_number = version_number
# Modus van de zender (3 bits)
self.mode = mode
# Het niveau van "lagen" tijdstip (1 byte)
self.stratum = 0
# Interval tussen verzoeken (1 byte)
self.pool = 0
# Precisie (log2) (1 byte)
self.precision = 0
# Interval voor de klok om de NTP-server te bereiken (4 bytes)
self.root_delay = 0
# Verspreiding van de klok NTP-server (4 bytes)
self.root_dispersion = 0
# Indicator van klokken (4 bytes)
self.ref_id = 0
# Laatste updating tijd op server (8 bytes)
self.reference = 0
# Tijdstip van verzending van pakket van lokale machine (8 bytes)
self.originate = 0
# Tijdstip van ontvangst op server (8 bytes)
self.receive = 0
# Tijdstip van antwoord verzenden van server (8 bytes)
self.transmit = transmit
Om een pakket naar de server te sturen (en te ontvangen), moeten we het in een bytearray kunnen omzetten.
Voor deze (en omgekeerde) operatie schrijven we twee functies — pack() en unpack():
Functie 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))
Functie 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
Voor luie mensen, als bijlage – een code die een pakket omzet in een mooie string
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)
Pakket verzenden naar de server
Er moet een pakket naar de server worden verzonden met ingevulde velden Versie, Modus en Verzendwordt een tabel met wijzigingen en instructies voor de overgang naar de nieuwe configuratie gegeven. Voor meer informatie, zie Verzend Het is nodig om de huidige tijd op de lokale machine op te geven (aantal seconden sinds 1 januari 1900), versie – elke van 1-4, mode – 3 (clientmode).
De server, die het verzoek ontvangt, vult alle velden in het NTP-pakket in door waarden te kopiëren naar het veld Origineer uit de Verzend, ontvangen in het verzoek. Het blijft voor mij een raadsel waarom de client zijn tijdwaarde niet direct in het veld kan invullen Origineer. Uiteindelijk heeft de client als het pakket terugkomt 4 tijdwaarden – tijd van het verzenden van het verzoek (Origineer), tijd van ontvangst van het verzoek door de server (Ontvang), tijd van verzenden van het antwoord door de server (Verzend) en tijd van ontvangst van het antwoord door de client – Aankomst (niet in het pakket). Met deze waarden kunnen we de juiste tijd vaststellen.
Code voor het verzenden en ontvangen van het pakket
# 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)
Gegevensverwerking van de server
De verwerking van gegevens van de server is vergelijkbaar met de acties van een Engelse gentleman uit de oude puzzel van Raymond M. Smullyan (1978): "Een man had geen horloge, maar thuis hing een accurate wandklok die hij soms vergat op te wounden. Een keer, weer vergeetachtig, ging hij op bezoek bij een vriend, bracht daar de avond door en kon bij terugkomst thuis de klok correct zetten. Hoe slaagde hij daarin, als de reistijd van tevoren niet bekend was?" Het antwoord is: "Bij het verlaten van zijn huis woundt de man de klok op en onthoudt hij in welke positie de wijzers staan. Bij aankomst bij zijn vriend en bij het verlaten van het huisje noteert hij de tijd van binnenkomst en vertrek. Dit stelt hem in staat te weten hoe lang hij bij zijn vriend was. Bij thuiskomst kijkt de man op de klok en bepaalt hij de duur van zijn afwezigheid. Door de tijd die hij bij zijn vriend doorbracht van deze tijd af te trekken, ontdekt hij de tijd die hij aan de heen- en terugreis heeft besteed. Door de helft van de heenreis tijd bij de tijd van vertrek bij zijn vriend op te tellen, kan hij het tijdstip berekenen waarop hij thuis aankomt en zijn klok dienovereenkomstig aanpassen."
We bepalen de verwerkingstijd van de server voor het verzoek:
- We bepalen de reistijd van het pakket van de klant naar de server: ((Aankomst - Oorspronkelijk) - (Verzenden - Ontvangen)) / 2
- We bepalen het verschil tussen de tijd van de klant en de server:
Ontvangen - Oorspronkelijk - ((Aankomst - Oorspronkelijk) - (Verzenden - Ontvangen)) / 2 =
2 * Ontvangen - 2 * Oorspronkelijk - Aankomst + Oorspronkelijk + Verzenden - Ontvangen =
Ontvangen - Oorspronkelijk - Aankomst + Verzenden
Voeg de verkregen waarde toe aan de lokale tijd en geniet van het leven.
Resultaat weergeven
time_different = answer.get_time_different(arrive_time)
result = "Tijdverschil: {}nServer tijd: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Nuttig .
Bron: habr.com
