Tous droits réservés

J'ai demandé l'heure à un serveur

Vous ne devinerez jamais ce qu'il a répondu !

L’autre jour, mon ordinateur professionnel a passé quelques dizaines de minutes avec son horloge décalée d’un peu moins d’une heure, alors que pourtant il se synchronise régulièrement avec un serveur quelque part sur le réseau.

Cela a éveillé ma curiosité sur comment tout cela fonctionne !

NTP, un protocole pour connaître l'heure

Sur les réseaux informatiques, les machines synchronisent leurs horloges en s’échangeant des messages selon le protocole NTP (Network Time Protocol).

C’est un protocole relativement complexe, qui explique comment les différentes machines doivent communiquer entre elles et synchroniser leurs horloges pour que tout se passe bien et que tout le monde soit bien à la même heure. La RFC 5905 en propose une description complète. Quand on ne programme pas un serveur de temps un peu costaud, on peut se contenter de la version simplifiée, décrite dans la RFC 4330.

Le protocole définit une hiérarchie de serveurs de qualité décroissante :

  • la strate 1, des serveurs qui obtiennent leur heure directement d’une horloge de référence (comme le GPS ou Galileo) ;
  • la strate 2, des serveurs qui obtiennent leur heure de serveurs de la strate 1,
  • etc.

Les serveurs de strate 1 ne devraient pas être interrogés directement par le commun des mortels. Donc, on reste poli et on interroge seulement des serveurs de strate 2 ou plus. Mais on peut faire encore mieux que de spammer un serveur en particulier, et faire appel à un pool de serveur, pour peu qu’on ne soit pas trop exigeant sur la précision.

Le protocole utilise UDP sur le port 123.

Dans son plus simple appareil, demander l’heure est très simple :

  • on envoie une requête au serveur de son choix ;
  • on attend sa réponse qui contient un timestamp.

Il suffit alors de faire quelques calculs pour connaître l’heure UTC.

Si on est courageux, on peut gérer un peu plus de complexité pour obtenir une heure plus précise, mais je ne le suis pas.

Python, va demander l'heure au monsieur

En lisant les RFC, on voit qu’on peut se contenter d’envoyer un paquet avec vraiment très peu d’information. Parmi tous les champs NTP, il suffit en effet d’indiquer :

  • la version du protocole (4 dans notre cas) ;
  • le mode (3 dans notre cas, ce qui veut dire qu’on est un client).

On peut laisser le reste à zéro.

En retour, le serveur nous répond plein de choses, dont la seule qui m’intéresse réellement est le timestamp (et encore seulement les secondes). Il s’agit du nombre de secondes depuis le 1er janvier 1900 qu’on convertit aisément en date et qu’on affiche.

J’ai fait un script Python (un peu sale) qui fait le job !

import socket
import datetime

# NTP server
host = "pool.ntp.org"
# NTP port 
port = 123
# Length of the NTP header sent or received (bytes)
header_length = 48
# Offset of the NTP timestamp field (bytes)
timestamp_offset = 40
# Origin of the NTP timestamp
dt = datetime.datetime(1900, 1, 1)

# Query the NTP server
s = socket.socket(type=socket.SOCK_DGRAM)
request = b"\x23" + b"\x00" * (header_length - 1)
s.sendto(request, (host, port))
d, _ = s.recvfrom(header_length)

# Compute and display the date
ts = int(d[timestamp_offset]) * 256 ** 3 + int(d[timestamp_offset + 1]) * 256 ** 2 + int(d[timestamp_offset + 2]) * 256 ** 1 + int(d[timestamp_offset + 3])
now = dt + datetime.timedelta(seconds=ts)
print(f"{now}")

Il s’utilise comme cela :

$ python3 ntp.py
2021-03-14 21:34:14

12 commentaires

Petite simplification avec Python : le type int possède une méthode from_bytes qui permet simplement de convertir une chaîne d’octets en nombre, elle prend pour cela là chaîne et un deuxième argument qui indique l’ordre des octets.
On peut alors remplacer la ligne 22 par quelque chose comme :

ts = int.from_bytes(d[timestamp_offset:timestamp_offset+4], 'big')

Je voulais aussi proposer de passer par datetime.datetime.fromtimestamp avant de voir que ce n’était pas le même epoch qui était utilisé.

Ah, c’est bien tout ça, je connaissais pas, et j’ai pas trouvé en cherchant rapidement !

D’ailleurs, @entwanne, je suis étonné que tu n’aies pas fait de remarque sur le fait que socket peut s’utiliser avec un gestionnaie de contexte (et que dans mon code, je ne ferme pas le socket du coup).

Je voulais aussi proposer de passer par datetime.datetime.fromtimestamp avant de voir que ce n’était pas le même epoch qui était utilisé.

La RFC dit même en substance « démerdez-vous pour convertir vers les formats de temps système ».

+1 -0

D’ailleurs, @entwanne, je suis étonné que tu n’aies pas fait de remarque sur le fait que socket peut s’utiliser avec un gestionnaie de contexte (et que dans mon code, je ne ferme pas le socket du coup).

Dans cet exemple précis, c’est un peu accessoire : le socket sera de toute façon fermé à la fin du processus. C’est pas comme si ce socket était lié à un port TCP (où là c’est beaucoup plus gênant de ne pas le libérer proprement).

+3 -0

La RFC dit même en substance « démerdez-vous pour convertir vers les formats de temps système ».

Effectivement. L’important c’est de savoir le format envoyé par ton serveur (petit boutiste ou gros boutiste) et c’est à toi de faire la conversion (le serveur ne sait pas forcément si tes entiers sont encodés selon de format petit boutiste ou gros boutiste).

Bref, tu as mis le doigt dans quelque chose qui existait bien avant les Web Services, les API JSON, etc. Normal que ça surprenne un peu.

Bonjour,

Intéressant. Mais comment fait-on pour gérer le décalage dû au temps de trajet des paquets ?

Pour une heure précise à la seconde normalement ça marche, mais si on veut l’heure précise à 100ms ou 10ms près, comment le gère-t-on ?

+1 -0

Bonjour,

Intéressant. Mais comment fait-on pour gérer le décalage dû au temps de trajet des paquets ?

Pour une heure précise à la seconde normalement ça marche, mais si on veut l’heure précise à 100ms ou 10ms près, comment le gère-t-on ?

QuentinC

Aabu a montré la commande simplifiée, mais la synchronisation NTP est beaucoup plus complexe que ça et inclut notamment plusieurs échanges avec calcul du ping.

+0 -0

Dans le billet, j’ai utilisé la version la plus simple possible : envoyer un paquet aussi vide que possible et lire l’heure d’émission de la réponse. Je n’ai d’ailleurs pris que les secondes, mais on a accès à une partie fractionnaire pour plus de chiffres significatifs. Et on a aussi plein de méthodes pour se synchroniser vraiment bien.

La RFC donne un schéma pour le protocole de synchronisation de base. Comme je pense qu’il te sera peu accessible, je t’explique globalement l’idée.

Les trames NTP permettent de stocker 4 timestamps. Le principe est de faire un aller retour avec le serveur. Pour simplifier :

  • quand la requête est émise, le client remplit un premier champ ;
  • quand le serveur reçoit la requête, il remplit un deuxième champ ;
  • quand le serveur émet la réponse, il remplit un troisième champ ;
  • quand le client reçoit la réponse, il remplit le dernier champ.

On a 4 valeurs, qui permettent de calculer l’écart entre les horloges et de mesurer le temps d’aller-retour. Cela permet d’améliorer la précision en éliminant le temps de transport et de traitement.

Pour faire encore mieux, le protocole décrit toute une série de procédures pour se synchroniser plus finement, à l’aide d’estimation statistiques de la qualité des horloges du système et d’algorithme pour asservir en fréquence. Je t’avoue que j’ai survolé, donc je ne saurai pas en dire plus. :D


Édit. : Dans le lien de nohar, on voit où mènent les raffinements, avec notamment de l’horodatage niveau hardware pour éviter les temps de traitement et leurs conséquences sur la précision.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte