Hallo habrayuzery. Heute möchte ich darüber sprechen, wie ich meinen einfachen NTP-Client schreibe. Grundsätzlich werden wir über die Struktur des Pakets und die Verarbeitung der Antwort vom NTP-Server sprechen. Der Code wird in Python geschrieben, weil meiner Meinung nach die beste Sprache für solche Dinge einfach nicht gefunden werden kann. Kenner werden auf die Ähnlichkeit des Codes mit dem ntplib-Code achten - ich war davon „inspiriert“.
Was genau ist NTP? NTP - Protokoll der Interaktion mit genauen Zeitservern. Dieses Protokoll wird in vielen modernen Maschinen verwendet.
Zum Beispiel der Dienst w32tm in Windows.Insgesamt gibt es 5 Versionen des NTP-Protokolls. Die erste, 0. Version (1985, RFC958)) gilt derzeit als veraltet. Jetzt werden die neueren verwendet, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) und 4. (1996, RFC2030). 1-4 Versionen sind miteinander kompatibel, sie unterscheiden sich nur in den Serverbetriebsalgorithmen.
Paketformat
Sprungindikator (Korrekturindikator) - eine Zahl, die eine Warnung vor der zweiten Koordination anzeigt. Wert:
- 0 - keine Korrektur
- 1 - Die letzte Minute des Tages enthält 61 Sekunden
- 2 - Die letzte Minute des Tages enthält 59 Sekunden
- 3 - Serverstörung (Zeit nicht synchronisiert)
Versionsnummer - Die Versionsnummer des NTP-Protokolls (1-4).
Modus - Betriebsmodus des Paketsenders. Wert von 0 bis 7, am häufigsten:
- 3 - Kunde
- 4 - Server
- 5 - Sendemodus
Schicht (Layering-Ebene) - Die Anzahl der Zwischenschichten zwischen dem Server und der Referenzuhr (1 - Der Server entnimmt Daten direkt von der Referenzuhr, 2 - Der Server nimmt Daten vom Server mit der Ebene 1 usw.).
Poll ist eine vorzeichenbehaftete Ganzzahl, die das maximale Intervall zwischen aufeinanderfolgenden Nachrichten darstellt. Hier gibt der NTP-Client das Intervall an, in dem er erwartet, den Server abzufragen, und der NTP-Server gibt das Intervall an, in dem er erwartet, abgefragt zu werden. Der Wert ist der binäre Logarithmus von Sekunden.
Präzision ist eine vorzeichenbehaftete Ganzzahl, die die Genauigkeit der Systemuhr darstellt. Der Wert ist der binäre Logarithmus von Sekunden.
Root-Verzögerung (Server-Verzögerung) - Die Zeit, während der die Uhr den NTP-Server erreicht, als Anzahl der Sekunden mit einem festen Punkt.
Root-Dispersion - Die Streuung der NTP-Serveruhr als Anzahl der Sekunden mit einem festen Punkt.
Ref id (Quellkennung) - ID der Uhr. Wenn der Server eine Schicht von 1 hat, ist ref id der Name der Atomuhr (4 ASCII-Zeichen). Wenn der Server einen anderen Server verwendet, wird die Adresse dieses Servers in die Referenz-ID geschrieben.
Die letzten 4 Felder sind Zeit - 32 Bit - der ganzzahlige Teil, 32 Bit - Bruchteil.
Referenz - die letzte Uhr auf dem Server.
Ursprung - der Zeitpunkt, zu dem das Paket gesendet wurde (vom Server ausgefüllt - mehr dazu weiter unten).
Empfangszeit, zu der das Paket vom Server empfangen wurde.
Sendezeit, zu der das Paket vom Server an den Client gesendet wurde (vom Client ausgefüllt, mehr dazu weiter unten).
Wir werden die letzten beiden Felder nicht berücksichtigen.
Schreiben wir unser Paket:
Paketcodeclass NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0):
Um ein Paket an den Server zu senden (und zu empfangen), müssen wir es in ein Array von Bytes umwandeln können.
Für diese (und umgekehrte) Operation schreiben wir zwei Funktionen - pack () und unpack ():
Packfunktion 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))
Um den Bruchteil der Zahl zum Schreiben in das Paket auszuwählen, benötigen wir die Funktion get_fraction ():
get_fraction () def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
Funktion auspacken def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6
Für faule Leute als Anwendung - ein Code, der ein Paket in eine schöne Zeichenfolge verwandelt 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)
Senden eines Pakets an den Server
Es ist erforderlich, ein Paket mit den ausgefüllten Feldern
Version ,
Modus und Senden an den Server zu senden. In
Senden müssen Sie die aktuelle Zeit auf dem lokalen Computer (Anzahl der Sekunden seit dem 1. Januar 1900), Version - beliebig 1-4, Modus - 3 (Client-Modus) angeben.
Nachdem der Server die Anforderung akzeptiert hat, füllt er alle Felder im NTP-Paket aus, indem er den
Übertragungswert aus der Anforderung in das Feld
Originate kopiert. Es ist mir ein Rätsel, warum der Kunde den Wert seiner Zeit nicht sofort in das Feld "
Ursprung" eingeben kann. Wenn das Paket zurückkommt, hat der Client viermal - die Zeit, zu der die Anforderung gesendet wurde (
Originate ), die Zeit, zu der der Server die Anforderung
empfangen hat (
Receive ), die Zeit, zu der der Server die Antwort gesendet hat (
Transmit ) und die Zeit, zu der der Client die Antwort empfangen hat -
Arrive (nicht im Paket). Mit diesen Werten können wir die richtige Zeit einstellen.
Paket senden und empfangen Code Daten vom Server verarbeiten
Die Verarbeitung von Daten vom Server ähnelt den Aktionen des englischen Gentleman aus der alten Aufgabe von Raymond M. Sullian (1978): „Eine Person hatte keine Uhr, aber andererseits gab es eine genaue Wanduhr, die er manchmal vergessen hatte zu starten. Einmal, nachdem er vergessen hatte, die Uhr wieder zu starten, besuchte er seinen Freund, verbrachte den Abend an diesem Ort und als er nach Hause zurückkehrte, gelang es ihm, die Uhr richtig einzustellen. Wie hat er das geschafft, wenn die Reisezeit nicht im Voraus bekannt war? “ Die Antwort lautet: „Eine Person verlässt das Haus, startet die Uhr und merkt sich, in welcher Position sich die Zeiger befinden. Als er zu einem Freund kommt und die Gäste verlässt, notiert er die Zeit seiner Ankunft und Abreise. Auf diese Weise kann er herausfinden, wie viel er besucht hat. Nach der Rückkehr nach Hause und dem Blick auf die Uhr bestimmt eine Person die Dauer ihrer Abwesenheit. Wenn man von dieser Zeit die Zeit abzieht, die er für einen Besuch aufgewendet hat, lernt eine Person die Zeit, die sie für die Hin- und Herreise aufgewendet hat. Nachdem er die Hälfte der für die Reise aufgewendeten Zeit zur Zeit des Verlassens der Gäste hinzugefügt hat, hat er die Möglichkeit, die Ankunftszeit zu Hause herauszufinden und die Uhrzeiger entsprechend zu übersetzen. “
Wir finden die Serverarbeitszeit auf der Anfrage:
- Wir finden die Paketpfadzeit vom Client zum Server: ((Arrive - Originate) - (Transmit - Receive)) / 2
- Finden Sie den Unterschied zwischen Client- und Serverzeit:
Empfangen - Originieren - ((Ankommen - Originieren) - (Senden - Empfangen)) / 2 =
2 * Empfangen - 2 * Originieren - Ankommen + Originieren + Senden - Empfangen =
Empfangen - Ursprünglich - Ankommen + Senden
Fügen Sie den erhaltenen Wert zur Ortszeit hinzu und genießen Sie das Leben.
Ausgabeergebnis time_different = answer.get_time_different(arrive_time) result = "Time difference: {}\nServer time: {}\n{}".format( time_different, datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"), answer.to_display()) print(result)
Nützlicher
Link .