Salve, utenti di Habr. Oggi voglio parlarvi di come scrivere il proprio semplice client NTP. Principalmente, discuteremo della struttura del pacchetto e del modo in cui elaborare la risposta dal server NTP. Il codice sarà scritto in Python, poiché, a mio avviso, non c'è linguaggio migliore per questo tipo di cose. Gli esperti noteranno la somiglianza del codice con quello di ntplib — mi sono 'ispirato' proprio a lui.
Quindi, cos'è esattamente l'NTP? L'NTP è un protocollo di interazione con i server di tempo preciso. Questo protocollo è utilizzato in molte macchine moderne. Ad esempio, il servizio w32tm in Windows.
Esistono in totale 5 versioni del protocollo NTP. La prima, la versione 0 (1985, RFC958), al momento è considerata obsoleta. Attualmente sono in uso versioni più recenti: la 1 (1988, RFC1059), la 2 (1989, RFC1119), la 3 (1992, RFC1305) e la 4 (1996, RFC2030). Le versioni 1-4 sono compatibili tra loro, differiscono solo negli algoritmi di funzionamento. server.
Formato del pacchetto

Leap indicator (indicatore di correzione) — numero che mostra un avviso sulla seconda di coordinazione. Valore:
- 0 – nessuna correzione
- 1 – l'ultima minuto del giorno contiene 61 secondi
- 2 – l'ultima minuto del giorno contiene 59 secondi
- 3 – errore del server (tempo non sincronizzato)
Numero di versione (numero di versione) – numero della versione del protocollo NTP (1-4).
Modalità (modalità) — modalità operativa del mittente del pacchetto. Valori da 0 a 7, i più comuni:
- 3 – cliente
- 4 – server
- 5 – modalità broadcast
Stratum (livello di stratificazione) – numero di strati intermedi tra il server e gli orologi di riferimento (1 – il server ottiene dati direttamente dagli orologi di riferimento, 2 – il server ottiene dati da un server di livello 1, ecc.).
Pool – numero intero firmato che rappresenta l'intervallo massimo tra messaggi consecutivi. Il client NTP specifica qui l'intervallo con cui intende interpellare il server, mentre il server NTP indica l'intervallo con cui prevede di essere interpellato. Il valore è il logaritmo binario dei secondi.
Precisione (precisione) — numero intero firmato che rappresenta la precisione degli orologi di sistema. Il valore è il logaritmo binario dei secondi.
Ritardo radice (ritardo del server) – tempo impiegato dai segnali degli orologi per raggiungere il server NTP, in secondi come numero a punto fisso.
Dispersione radice (dispersione delle letture del server) — dispersione delle letture dell'orologio NTP come numero di secondi con punto fisso.
Ref id (identificatore sorgente) – id dell'orologio. Se il server ha stratum 1, ref id è il nome dell'orologio atomico (4 simboli ASCII). Se il server utilizza un altro server, l'indirizzo di quel server è registrato in ref id.
Gli ultimi 4 campi rappresentano il tempo – 32 bit – parte intera, 32 bit – parte frazionaria.
Riferimento — ultime letture dell'orologio sul server.
Origine – tempo in cui il pacchetto è stato inviato (compilato dal server – di questo si parlerà più avanti).
Ricevi – tempo di ricezione del pacchetto da parte del server.
Trasmetti – tempo di invio del pacchetto dal server al cliente (compilato dal cliente, di questo si parlerà anche più avanti).
Non considereremo i due ultimi campi.
Scriviamo il nostro pacchetto:
Codice pacchetto
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Necessario di inserire il secondo supplementare (2 bit)
self.leap_indicator = 0
# Versione del protocollo (3 bit)
self.version_number = version_number
# Modalità del mittente (3 bit)
self.mode = mode
# Livello di "stratificazione" dell'orario (1 byte)
self.stratum = 0
# Intervallo tra le richieste (1 byte)
self.pool = 0
# Precisione (log2) (1 byte)
self.precision = 0
# Intervallo per il raggiungimento del server NTP (4 byte)
self.root_delay = 0
# Dispersione dell'orologio del server NTP (4 byte)
self.root_dispersion = 0
# Indicatore degli orologi (4 byte)
self.ref_id = 0
# Tempo dell'ultimo aggiornamento sul server (8 byte)
self.reference = 0
# Tempo di invio del pacchetto dalla macchina locale (8 byte)
self.originate = 0
# Tempo di ricezione sul server (8 byte)
self.receive = 0
# Tempo di invio della risposta dal server (8 byte)
self.transmit = transmit
Per inviare (e ricevere) un pacchetto al server, dobbiamo saperlo convertire in un array di byte.
Per questa (e per l'operazione inversa) scriveremo due funzioni: pack() e unpack():
Funzione 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))
Funzione 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
Per i pigri, ecco un'app – un codice che trasforma il pacchetto in una bella stringa
def to_display(self):
return "Indicatore di bisestile: {0.leap_indicator}n"
"Numero di versione: {0.version_number}n"
"Modalità: {0.mode}n"
"Stratum: {0.stratum}n"
"Pool: {0.pool}n"
"Precisione: {0.precision}n"
"Ritardo radice: {0.root_delay}n"
"Dispersione radice: {0.root_dispersion}n"
"ID riferimento: {0.ref_id}n"
"Riferimento: {0.reference}n"
"Origine: {0.originate}n"
"Ricezione: {0.receive}n"
"Trasmissione: {0.transmit}"
.format(self)
Invio del pacchetto al server
È necessario inviare un pacchetto al server con i campi compilati Versione, Modalità e Trasmetti. In Trasmetti è necessario specificare l'ora corrente sulla macchina locale (numero di secondi dal 1 gennaio 1900), versione – qualsiasi da 1 a 4, modalità – 3 (modalità client).
Il server, ricevuto il comando, compila nel pacchetto NTP tutti i campi, copiando nel campo Origine il valore da Trasmetti, ricevuto nella richiesta. Non capisco perché il client non possa subito compilare il valore del proprio tempo nel campo Origine. Risultato, quando il pacchetto torna indietro, il client ha 4 valori di tempo – il tempo di invio della richiesta (Origine), il tempo di ricezione della richiesta da parte del server (Ricevi), il tempo di invio della risposta dal server (Trasmetti) e il tempo di ricezione della risposta dal client – Arrivo (non presente nel pacchetto). Con questi valori possiamo impostare l'ora corretta.
Codice di invio e ricezione del pacchetto
# 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)
Elaborazione dei dati dal server
Il trattamento dei dati dal server è simile alle azioni di un gentiluomo inglese da un'antica questione di Raymond M. Smullyan (1978): «Una persona non aveva un orologio da polso, ma a casa c'erano orologi da parete precisi che a volte dimenticava di caricare. Un giorno, dimenticando di caricare nuovamente l'orologio, andò a far visita a un amico, trascorse la serata con lui e, tornando a casa, riuscì a impostare correttamente l'orologio. Come è riuscito a farlo se il tempo di viaggio non era noto in anticipo?» La risposta è la seguente: «Uscendo di casa, la persona carica l'orologio e ricorda in che posizione si trovano le lancette. Arrivato dall'amico e andando via, segna l'ora del suo arrivo e della sua partenza. Questo gli permette di sapere quanto tempo ha trascorso a casa dell'amico. Tornato a casa e guardando l'orologio, la persona determina la durata della sua assenza. Sottraendo da questo tempo quello che ha trascorso a casa dell'amico, conosce il tempo impiegato per il viaggio di andata e ritorno. Aggiungendo a quell'ora il mezzo tempo speso nel viaggio, riesce a determinare l'ora di arrivo a casa e a regolare di conseguenza le lancette del suo orologio.»
Stiamo trovando il tempo di risposta del server alla richiesta:
- Stiamo trovando il tempo di viaggio del pacchetto dal cliente al server: ((Arrivo – Origine) — (Trasmetti – Ricevi)) / 2
- Stiamo trovando la differenza tra il tempo del cliente e quello del server:
Ricevi — Origine — ((Arrivo – Origine) — (Trasmetti – Ricevi)) / 2 =
2 * Ricevi — 2 * Origine – Arrivo + Origine + Trasmetti – Ricevi =
Ricevi – Origine – Arrivo + Trasmetti
Aggiungiamo il valore ottenuto all'orario locale e godiamoci la vita.
Risultato
time_different = answer.get_time_different(arrive_time)
result = "Differenza di tempo: {}nTempo del server: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Utile .
Fonte: habr.com
