Skryf 'n eenvoudige NTP-kliënt

Hallo misbruikers. Vandag wil ek praat oor hoe om jou eie eenvoudige NTP-kliënt te skryf. Basies sal die gesprek draai na die struktuur van die pakkie en hoe die reaksie vanaf die NTP-bediener verwerk word. Die kode sal in luislang geskryf word, want na my mening is daar eenvoudig geen beter taal vir sulke dinge nie. Fynproewers sal aandag gee aan die ooreenkoms van die kode met die ntplib-kode – ek is daardeur “geïnspireer”.

So, wat is NTP in elk geval? NTP is 'n protokol om met tydbedieners te kommunikeer. Hierdie protokol word in baie moderne masjiene gebruik. Byvoorbeeld, w32tm diens in Windows.

Daar is altesaam 5 weergawes van die NTP-protokol. Die eerste weergawe 0 (1985, RFC958) word tans as verouderd beskou. Nuwers word tans gebruik, 1ste (1988, RFC1059), 2de (1989, RFC1119), 3de (1992, RFC1305) en 4de (1996, RFC2030). Weergawes 1-4 is versoenbaar met mekaar, hulle verskil slegs in die algoritmes van die bedieners.

Pakkie formaat

Skryf 'n eenvoudige NTP-kliënt

Spring aanwyser (korreksie-aanwyser) is 'n nommer wat die skrikkel-sekonde-waarskuwing aandui. Betekenis:

  • 0 - geen regstelling nie
  • 1 - die laaste minuut van die dag bevat 61 sekondes
  • 2 - die laaste minuut van die dag bevat 59 sekondes
  • 3 - bedienerfout (tyd uit sinchroniseer)

Weergawe nommer (weergawenommer) – NTP-protokol weergawenommer (1-4).

af (modus) — werkingsmodus van die pakkiesender. Waarde van 0 tot 7, mees algemeen:

  • 3 - kliënt
  • 4 - bediener
  • 5 - uitsaaimodus

stratum (laagvlak) - die aantal tussenlae tussen die bediener en die verwysingsklok (1 - die bediener neem data direk vanaf die verwysingsklok, 2 - die bediener neem data van die bediener met vlak 1, ens.).
Swembad is 'n getekende heelgetal wat die maksimum interval tussen opeenvolgende boodskappe verteenwoordig. Die NTP-kliënt spesifiseer hier die interval waarteen dit verwag om die bediener te poll, en die NTP-bediener spesifiseer die interval waarteen dit verwag om ondervra te word. Die waarde is gelyk aan die binêre logaritme van sekondes.
Presisie (presisie) is 'n getekende heelgetal wat die akkuraatheid van die stelselklok voorstel. Die waarde is gelyk aan die binêre logaritme van sekondes.
Wortel vertraging (bedienervertraging) is die tyd wat dit neem vir die klok om die NTP-bediener te bereik, as 'n vaste punt aantal sekondes.
wortelverspreiding (bedienerverstrooiing) - Die verstrooiing van die NTP-bedienerklok as 'n vaste punt aantal sekondes.
Verwys id (bron-ID) – kyk-ID. As die bediener stratum 1 het, dan is ref id die naam van die atoomhorlosie (4 ASCII karakters). As die bediener 'n ander bediener gebruik, dan bevat die ref id die adres van hierdie bediener.
Die laaste 4 velde is die tyd - 32 bisse - die heelgetaldeel, 32 bisse - die breukdeel.
verwysing - die nuutste klok op die bediener.
Ontstaan – tyd wanneer die pakkie gestuur is (deur die bediener ingevul – meer daaroor hieronder).
Voorkeur – tyd wanneer die pakkie deur die bediener ontvang is.
Oordra – tyd wanneer die pakkie vanaf die bediener na die kliënt gestuur is (ingevuld deur die kliënt, meer daaroor hieronder).

Die laaste twee velde sal nie oorweeg word nie.

Kom ons skryf ons pakkie:

Pakket kode

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

Om 'n pakkie na die bediener te stuur (en te ontvang), moet ons dit in 'n verskeidenheid grepe kan verander.
Vir hierdie (en omgekeerde) operasie, sal ons twee funksies skryf - pack() en unpack():

pak funksie

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

uitpak funksie

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

Vir lui mense, as 'n toepassing - kode wat die pakkie in 'n pragtige string verander

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)

Stuur 'n pakket na die bediener

Stuur 'n pakkie met gevulde velde na die bediener weergawe, af и Oordra. In Oordra jy moet die huidige tyd op die plaaslike masjien spesifiseer (aantal sekondes sedert 1 Januarie 1900), weergawe - enige van 1-4, modus - 3 (kliëntmodus).

Die bediener, nadat die versoek ontvang is, vul al die velde in die NTP-pakkie in en kopieer in die veld Ontstaan waarde van Oordra, wat in die versoek gekom het. Dit is vir my 'n raaisel hoekom die kliënt nie dadelik die waarde van sy tyd in die veld kan invul nie Ontstaan. As gevolg hiervan, wanneer die pakkie terugkom, het die kliënt 4 tydwaardes - die tyd wat die versoek gestuur is (Ontstaan), die tyd wat die bediener die versoek ontvang het (Voorkeur), die tyd wat die bediener die antwoord gestuur het (Oordra) en die tyd van ontvangs van 'n antwoord deur die kliënt - kom (nie in die pakkie nie). Met hierdie waardes kan ons die regte tyd instel.

Pakket stuur en ontvang kode

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

Dataverwerking vanaf die bediener

Die verwerking van data vanaf die bediener is soortgelyk aan die optrede van die Engelse heer uit die ou probleem van Raymond M. Smallian (1978): “Een persoon het nie ’n polshorlosie gehad nie, maar daar was ’n akkurate muurhorlosie by die huis, wat hy soms vergeet om te wind. Op 'n dag, toe hy vergeet om weer die horlosie te begin, het hy by sy vriend gaan kuier, die aand saam met hom deurgebring, en toe hy terugkom huis toe, het hy daarin geslaag om die horlosie reg te stel. Hoe het hy dit reggekry as die reistyd nie vooraf bekend was nie? Die antwoord is: “Wanneer jy die huis verlaat, draai iemand die horlosie op en onthou die posisie van die wysers. As hy na 'n vriend kom en die gaste verlaat, teken hy die tyd van sy aankoms en vertrek aan. Dit laat hom toe om uit te vind hoe lank hy weg was. As hy terugkom huis toe en na die horlosie kyk, bepaal 'n persoon die duur van sy afwesigheid. Deur die tyd wat hy besoek het, van hierdie tyd af te trek, vind die persoon uit hoeveel tyd op die pad heen en terug spandeer word. Deur die helfte van die tyd wat op die pad spandeer word by die tyd van die vertrek van die gaste te voeg, kry hy die geleentheid om die tyd van aankoms by die huis uit te vind en die wysers van sy horlosie daarvolgens aan te pas.

Vind die tyd wat die bediener aan die versoek gewerk het:

  1. Vind die pakkie reistyd van die kliënt na die bediener: ((Arriveer - Ontstaan) - (Send - Ontvang)) / 2
  2. Vind die verskil tussen kliënt- en bedienertyd:
    Ontvang - Oorsprong - ((Aankom - Oorsprong) - (Verstuur - Ontvang)) / 2 =
    2 * Ontvang - 2 * Origineer - Arriveer + Origineer + Versend - Ontvang =
    Ontvang - Oorsprong - Arriveer + Versend

Ons voeg die ontvangde waarde by die plaaslike tyd en geniet die lewe.

Resultaat Uitset

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)

Nuttig skakel.

Bron: will.com

Voeg 'n opmerking