Bonjour, habrayuzery. Aujourd'hui, je veux parler de la façon d'écrire mon simple client NTP. Fondamentalement, nous parlerons de la structure du paquet et de la façon de traiter la réponse du serveur NTP. Le code sera écrit en python, car, il me semble, le meilleur langage pour de telles choses ne peut tout simplement pas être trouvé. Les connaisseurs feront attention à la similitude du code avec le code ntplib - je m'en suis «inspiré».
Alors, quel est exactement NTP? NTP - protocole d'interaction avec des serveurs de temps exacts. Ce protocole est utilisé dans de nombreuses machines modernes.
Par exemple, le service w32tm dans Windows.Il existe 5 versions du protocole NTP au total. La première, la 0e version (1985, RFC958)) est actuellement considérée comme obsolète. Maintenant, les plus récents sont utilisés, 1er (1988, RFC1059), 2e (1989, RFC1119), 3e (1992, RFC1305) et 4e (1996, RFC2030). Les versions 1-4 sont compatibles les unes avec les autres, elles ne diffèrent que par les algorithmes de fonctionnement du serveur.
Format du paquet
Indicateur de saut (
indicateur de correction) - un nombre indiquant un avertissement concernant la deuxième coordination. Valeur:
- 0 - aucune correction
- 1 - la dernière minute de la journée contient 61 secondes
- 2 - la dernière minute de la journée contient 59 secondes
- 3 - dysfonctionnement du serveur (heure non synchronisée)
Numéro de version - Le numéro de version du protocole NTP (1-4).
Mode - mode de fonctionnement de l'expéditeur de paquets. Valeur de 0 à 7, la plus courante:
- 3 - client
- 4 - serveur
- 5 - mode de diffusion
Strate (niveau de superposition) - le nombre de couches intermédiaires entre le serveur et l'horloge de référence (1 - le serveur prend les données directement de l'horloge de référence, 2 - le serveur prend les données du serveur avec le niveau 1, etc.).
Poll est un entier signé représentant l'intervalle maximum entre les messages consécutifs. Ici, le client NTP indique l'intervalle auquel il s'attend à interroger le serveur et le serveur NTP indique l'intervalle auquel il s'attend à être interrogé. La valeur est le logarithme binaire des secondes.
La précision est un entier signé représentant la précision de l'horloge système. La valeur est le logarithme binaire des secondes.
Délai racine (
délai du serveur) - le temps pendant lequel l'horloge atteint le serveur NTP, comme le nombre de secondes avec un point fixe.
Dispersion racine - la dispersion de l'horloge du serveur NTP en nombre de secondes avec un point fixe.
Ref id (identifiant source) - id de la montre. Si le serveur a une strate de 1, alors ref id est le nom de l'horloge atomique (4 caractères ASCII). Si le serveur utilise un autre serveur, l'adresse de ce serveur est écrite dans l'ID de référence.
Les 4 derniers champs sont le temps - 32 bits - la partie entière, 32 bits - la partie fractionnaire.
Référence - la dernière horloge du serveur.
Originate - l'heure à laquelle le paquet a été envoyé (rempli par le serveur - plus de détails ci-dessous).
Receive - heure à laquelle le paquet a été reçu par le serveur.
Transmit - heure à laquelle le paquet a été envoyé du serveur au client (rempli par le client, plus à ce sujet ci-dessous).
Nous ne considérerons pas les deux derniers champs.
Écrivons notre package:
Code du packageclass NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0):
Pour envoyer (et recevoir) un paquet au serveur, nous devons être en mesure de le transformer en un tableau d'octets.
Pour cette opération (et inversée), nous allons écrire deux fonctions - pack () et unpack ():
Fonction 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))
Pour sélectionner la partie fractionnaire du nombre à écrire dans le package, nous avons besoin de la fonction get_fraction ():
get_fraction () def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
Fonction de déballage def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6
Pour les paresseux, comme une application - un code qui transforme un package en une belle chaîne 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)
Envoi d'un paquet au serveur
Il est nécessaire d'envoyer un paquet au serveur avec les champs
Version ,
Mode et
Transmit remplis. Dans
Transmit, vous devez spécifier l'heure actuelle sur la machine locale (nombre de secondes depuis le 1er janvier 1900), version - 1 à 4, mode - 3 (mode client).
Après avoir accepté la demande, le serveur remplit tous les champs du paquet NTP en copiant la valeur de
transmission de la demande dans le champ
Originate . C'est un mystère pour moi que le client ne puisse pas saisir immédiatement la valeur de son temps dans le champ
Originate . Par conséquent, lorsque le paquet revient, le client dispose de 4 fois - l'heure à laquelle la demande a été envoyée (
origine ), l'heure à laquelle le serveur a reçu la demande (
réception ), l'heure à laquelle le serveur a envoyé la réponse (
transmission ) et l'heure à laquelle le client a reçu la réponse - à l'
arrivée (pas dans le paquet). En utilisant ces valeurs, nous pouvons définir l'heure correcte.
Envoi et réception de colis Traitement des données du serveur
Le traitement des données du serveur est similaire aux actions du gentleman anglais de l'ancienne tâche de Raymond M. Sullian (1978): «Une personne n'avait pas de montre, mais d'autre part il y avait une horloge murale exacte qu'il oubliait parfois de démarrer. Une fois, ayant oublié de redémarrer la montre, il est allé rendre visite à son ami, a passé la soirée à cet endroit, et quand il est rentré chez lui, il a réussi à régler correctement l'horloge. Comment a-t-il réussi à faire cela si le temps de trajet n'était pas connu à l'avance? La réponse est: «En quittant la maison, une personne démarre la montre et se souvient de la position des aiguilles. Venant chez un ami et quittant les invités, il note l'heure de son arrivée et de son départ. Cela lui permet de savoir combien il visitait. De retour chez elle et en regardant la montre, une personne détermine la durée de son absence. Soustrayant de ce temps le temps qu'il a passé en visite, une personne apprend le temps passé sur le trajet aller-retour. Ayant ajouté la moitié du temps consacré au voyage au moment de quitter les invités, il a la possibilité de connaître l'heure d'arrivée à la maison et de traduire les aiguilles de l'horloge en conséquence. »
On retrouve le temps de travail du serveur sur demande:
- Nous trouvons le temps de chemin du paquet du client au serveur: ((Arrive - Originate) - (Transmit - Receive)) / 2
- Trouvez la différence entre l'heure du client et celle du serveur:
Recevoir - Émettre - ((Arriver - Émettre) - (Transmettre - Recevoir)) / 2 =
2 * Réception - 2 * Origine - Arrivée + Origine + Transmission - Réception =
Recevoir - Émettre - Arriver + Transmettre
Ajoutez la valeur obtenue à l'heure locale et profitez de la vie.
Résultat de sortie 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)
Lien utile.