Skribante simplan NTP-klienton

Saluton, Habrausers. Hodiaŭ mi volas paroli pri kiel skribi vian propran simplan NTP-klienton. Esence, la konversacio turniĝos al la strukturo de la pako kaj la metodo de prilaborado de la respondo de la NTP-servilo. La kodo estos skribita en Python, ĉar ŝajnas al mi, ke simple ne ekzistas pli bona lingvo por tiaj aferoj. Konatoj rimarkos la similecon de la kodo kun la ntplib-kodo - mi estis "inspirita" de ĝi.

Do kio ĝuste estas NTP? NTP estas protokolo por interagado kun ĝustatempaj serviloj. Ĉi tiu protokolo estas uzata en multaj modernaj maŝinoj. Ekzemple, la w32tm-servo en fenestroj.

Ekzistas entute 5 versioj de la NTP-protokolo. La unua, versio 0 (1985, RFC958), estas nuntempe konsiderata malaktuala. Pli novaj versioj estas nuntempe uzataj: versio 1 (1988, RFC1059), versio 2 (1989, RFC1119), versio 3 (1992, RFC1305), kaj versio 4 (1996, RFC2030). Versioj 1-4 estas kongruaj unu kun la alia; ili diferencas nur per siaj funkciaj algoritmoj. serviloj.

Formato de pako

Skribante simplan NTP-klienton

Salta indikilo (korekta indikilo) - nombro indikanta averton pri la kunordigo sekundo. Signifo:

  • 0 - neniu korekto
  • 1 – la lasta minuto de la tago enhavas 61 sekundojn
  • 2 – la lasta minuto de la tago enhavas 59 sekundojn
  • 3 - servila misfunkcio (la tempo ne estas sinkronigita)

Versio nombro (versionumero) - NTP-protokola versio numero (1-4).

reĝimo (reĝimo) — operacia reĝimo de la pakaĵeto. Valoro de 0 ĝis 7, plej ofta:

  • 3 - kliento
  • 4 – servilo
  • 5 - elsenda reĝimo

Tavolo (tavola nivelo) - la nombro da mezaj tavoloj inter la servilo kaj la referenca horloĝo (1 - la servilo prenas datumojn rekte de la referenca horloĝo, 2 - la servilo prenas datumojn de servilo kun tavolo 1, ktp.).
naĝejo estas signita entjero reprezentanta la maksimuman intervalon inter sinsekvaj mesaĝoj. La NTP-kliento precizigas ĉi tie la intervalon je kiu ĝi atendas baloti la servilon, kaj la NTP-servilo precizigas la intervalon je kiu ĝi atendas esti balotita. La valoro estas egala al la binara logaritmo de sekundoj.
precizeco (precizeco) estas signita entjero reprezentanta la precizecon de la sistema horloĝo. La valoro estas egala al la binara logaritmo de sekundoj.
Radika prokrasto (servila malfruo) - la tempo necesas por la horloĝlegadoj atingi la NTP-servilon, kiel fikspunkta nombro da sekundoj.
Radika disvastigo (servila disvastiĝo) - disvastiĝo de NTP-servilaj horloĝlegaĵoj kiel nombro da sekundoj kun fiksa punkto.
Ref id (fontoidentigilo) – horloĝidentigilo. Se la servilo havas tavolon 1, tiam ref id estas la nomo de la atoma horloĝo (4 ASCII-signoj). Se la servilo uzas alian servilon, tiam la ref id enhavas la adreson de ĉi tiu servilo.
La lastaj 4 kampoj reprezentas la tempon - 32 bitoj - la entjera parto, 32 bitoj - la frakcia parto.
referenco — la plej novaj horloĝoj sur la servilo.
Origini – tempo, kiam la pako estis sendita (plenigita de la servilo - pli pri tio ĉi sube).
ricevi – tempo kiam la pako estis ricevita de la servilo.
publikigi – tempo de sendado de la pako de la servilo al la kliento (plenigita de la kliento, pli pri tio ĉi sube).

Ni ne konsideros la lastajn du kampojn.

Ni skribu nian pakaĵon:

Pakokodo

class NTPPacket:
    _FORMAT = "!B B b b 11I"

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Necessary of enter leap second (2 bits)
        self.leap_indicator = 0
        # Version of protocol (3 bits)
        self.version_number = version_number
        # Mode of sender (3 bits)
        self.mode = mode
        # The level of "layering" reading time (1 byte)
        self.stratum = 0
        # Interval between requests (1 byte)
        self.pool = 0
        # Precision (log2) (1 byte)
        self.precision = 0
        # Interval for the clock reach NTP server (4 bytes)
        self.root_delay = 0
        # Scatter the clock NTP-server (4 bytes)
        self.root_dispersion = 0
        # Indicator of clocks (4 bytes)
        self.ref_id = 0
        # Last update time on server (8 bytes)
        self.reference = 0
        # Time of sending packet from local machine (8 bytes)
        self.originate = 0
        # Time of receipt on server (8 bytes)
        self.receive = 0
        # Time of sending answer from server (8 bytes)
        self.transmit = transmit

Por sendi (kaj ricevi) paketon al la servilo, ni devas povi turni ĝin en bajtan tabelon.
Por ĉi tiu (kaj inversa) operacio, ni skribos du funkciojn - pack() kaj unpack():

paka funkcio

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))

malpaki funkcion

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

Por maldiligentaj homoj, kiel aplikaĵo - kodo, kiu igas pakon en belan ŝnuron

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)

Sendante pakaĵon al la servilo

Pako kun plenigitaj kampoj devas esti sendita al la servilo versio, reĝimo и publikigi. la publikigi vi devas specifi la nunan tempon sur la loka maŝino (la nombro da sekundoj ekde la 1-a de januaro 1900), versio - iu ajn el 1-4, reĝimo - 3 (klienta reĝimo).

La servilo, akceptinte la peton, plenigas ĉiujn kampojn en la NTP-pako, kopiante en la kampon Origini valoro de publikigi, kiu venis en la peto. Estas mistero por mi, kial la kliento ne povas tuj plenigi la valoron de sia tempo en la kampo Origini. Kiel rezulto, kiam la pako revenas, la kliento havas 4 tempovalorojn - la tempo kiam la peto estis sendita (Origini), tempo kiam la servilo ricevis la peton (ricevi), tempo kiam la servilo sendis la respondon (publikigi) kaj la tempo kiam la kliento ricevis la respondon - Alvenu (ne en la pakaĵo). Uzante ĉi tiujn valorojn ni povas agordi la ĝustan tempon.

Pako sendanta kaj ricevanta kodon

# 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)

Prilaborado de datumoj de la servilo

Prilaborado de datumoj de la servilo similas al la agoj de la angla sinjoro el la malnova problemo de Raymond M. Smullyan (1978): „Unu viro ne havis pojnhorloĝon, sed estis preciza murhorloĝo hejme, kiun li foje forgesis. vento. Iun tagon, forgesinte reblovi sian horloĝon, li iris viziti sian amikon, pasigis la vesperon kun li, kaj kiam li revenis hejmen, li sukcesis ĝuste agordi la horloĝon. Kiel li sukcesis fari tion, se la vojaĝdaŭro ne estis antaŭsciita? La respondo estas: "Forlasante la hejmon, persono bobenas sian horloĝon kaj memoras en kiu pozicio estas la montriloj. Veninte al amiko kaj lasinte la gastojn, li notas la tempon de sia alveno kaj foriro. Ĉi tio permesas al li ekscii kiom longe li vizitis. Revenante hejmen kaj rigardante la horloĝon, homo determinas la daŭron de sia foresto. Subtrahante de ĉi tiu tempo la tempon, kiun li pasigis vizitante, homo ekscias la tempon pasigitan vojaĝante tien kaj reen. Aldonante duonon de la tempo pasigita survoje al la tempo de forlasado de la gastoj, li ricevas la ŝancon ekscii la horon de alveno hejmen kaj alĝustigi la montrilojn de sia horloĝo laŭe.”

Trovu la tempon kiam la servilo laboras pri peto:

  1. Trovu la vojaĝdaŭron de la pako de la kliento al la servilo: ((Alveni – Origini) – (Transsendi – Ricevi)) / 2
  2. Trovu la diferencon inter klienta kaj servila tempo:
    Ricevu - Origini - ((Alveni - Origini) - (Transsendi - Ricevi)) / 2 =
    2 * Ricevu - 2 * Origini - Alveni + Origini + Transsendi - Ricevu =
    Ricevu - Origini - Alveni + Transsendi

Ni aldonas la rezultan valoron al la loka tempo kaj ĝuas la vivon.

Eligo de la rezulto

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)

Utila ligilo.

fonto: www.habr.com

Aldoni komenton