Lihtsa NTP-kliendi kirjutamine

Tere habrauserid. Täna tahan rääkida sellest, kuidas kirjutada oma lihtsat NTP-klienti. Põhimõtteliselt keskendub vestlus paketi struktuurile ja sellele, kuidas NTP-serveri vastust töödeldakse. Kood kirjutatakse pythonis, sest minu meelest pole selliste asjade jaoks lihtsalt paremat keelt. Asjatundjad pööravad tähelepanu koodi sarnasusele ntplib koodiga – sain sellest "inspiratsiooni".

Mis siis ikkagi on NTP? NTP on ajaserveritega suhtlemise protokoll. Seda protokolli kasutatakse paljudes kaasaegsetes masinates. Näiteks teenus w32tm Windowsis.

Kokku on NTP-protokolli 5 versiooni. Esimest, versiooni 0 (1985, RFC958) peetakse praegu aegunuks. Praegu on kasutusel uuemad, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) ja 4. (1996, RFC2030). Versioonid 1-4 ühilduvad omavahel, erinevad ainult serverite algoritmide poolest.

Paketi vorming

Lihtsa NTP-kliendi kirjutamine

Hüppe indikaator (parandusindikaator) on number, mis näitab hüpesekundi hoiatust. Tähendus:

  • 0 – parandus puudub
  • 1 – päeva viimane minut sisaldab 61 sekundit
  • 2 - päeva viimane minut sisaldab 59 sekundit
  • 3 - serveri rike (aeg sünkroonimata)

Versiooni number (versiooninumber) – NTP protokolli versiooni number (1-4).

viis (režiim) — paketi saatja töörežiim. Väärtus vahemikus 0 kuni 7, kõige tavalisem:

  • 3 - klient
  • 4 - server
  • 5 - saaterežiim

Kiht (kihilisuse tase) - serveri ja võrdluskella vahel olevate vahekihtide arv (1 - server võtab andmeid otse võrdluskellast, 2 - server võtab andmeid 1. tasemega serverist jne).
Piljard on märgiga täisarv, mis tähistab maksimaalset intervalli järjestikuste sõnumite vahel. NTP-klient määrab siin intervalli, mille järel ta eeldab serveri küsitlust, ja NTP-server määrab intervalli, mille järel ta eeldab küsitlust. Väärtus võrdub sekundite binaarlogaritmiga.
Täpsus (täpsus) on märgiga täisarv, mis tähistab süsteemi kella täpsust. Väärtus võrdub sekundite binaarlogaritmiga.
juure viivitus (serveri latentsus) on aeg, mis kulub kella NTP-serverisse jõudmiseks fikseeritud sekundite arvuna.
Juurte dispersioon (serveri hajumine) – NTP-serveri kella hajumine fikseeritud punktiga sekundite arvuna.
Viite id (allika ID) – vaatamise id. Kui serveril on kiht 1, siis ref id on aatomkella nimi (4 ASCII märki). Kui server kasutab teist serverit, sisaldab ref id selle serveri aadressi.
Viimased 4 välja on aeg – 32 bitti – täisarvuline osa, 32 bitti – murdosa.
Viide - serveri uusim kell.
Päritolu – paketi saatmise aeg (server täidab – sellest lähemalt allpool).
Saama – aeg, millal server paketi vastu võttis.
Edastama – paketi serverist kliendile saatmise aeg (täidab klient, sellest lähemalt allpool).

Kahte viimast välja ei võeta arvesse.

Kirjutame oma paketi:

Paketi kood

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

Paketi saatmiseks (ja vastuvõtmiseks) serverisse peame suutma muuta selle baitide massiiviks.
Selle (ja vastupidise) toimingu jaoks kirjutame kaks funktsiooni - pack() ja unpack():

pakkimisfunktsioon

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

lahtipakkimise funktsioon

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

Laiskadele inimestele rakendusena - kood, mis muudab paki ilusaks stringiks

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)

Paki saatmine serverisse

Saada serverisse täidetud väljadega pakett versioon, viis и Edastama. Sisse Edastama peate määrama kohaliku masina praeguse kellaaja (sekundite arv alates 1. jaanuarist 1900), versioon - mis tahes 1-4, režiim - 3 (kliendirežiim).

Pärast päringu vastuvõtmist täidab server kõik NTP-paketi väljad, kopeerides väljale Päritolu väärtus alates Edastama, mis päringus kaasas. Minu jaoks on mõistatus, miks klient ei saa kohe täita oma põllul veedetud aja väärtust Päritolu. Selle tulemusena on paketi tagasitulekul kliendil 4 ajaväärtust - päringu saatmise aeg (Päritolu), aeg, mil server päringu kätte sai (Saama), aeg, mil server vastuse saatis (Edastama) ja kliendi poolt vastuse saamise aeg - Saabuma (ei ole pakendis). Nende väärtustega saame määrata õige aja.

Paki saatmise ja vastuvõtmise kood

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

Andmete töötlemine serverist

Andmete töötlemine serverist sarnaneb inglise härrasmehe tegudega Raymond M. Smalliani (1978) vanast probleemist: „Ühel inimesel ei olnud käekella, kuid kodus oli täpne seinakell, mille ta. mõnikord unustas tuulutada. Ühel päeval, unustades uuesti kella käima panna, läks ta sõbrale külla, veetis temaga õhtu ning koju naastes õnnestus kella õigeks seada. Kuidas ta sellega hakkama sai, kui reisiaeg polnud ette teada? Vastus on: “Majast lahkudes keerab inimene kella üles ja jätab meelde osutite asendi. Sõbra juurde tulles ja külalistest lahkudes märgib ta oma saabumise ja lahkumise aja. See võimaldab tal teada saada, kui kaua ta eemal oli. Koju naastes ja kella vaadates määrab inimene ära oma äraoleku kestuse. Lahutades sellest ajast külaskäigule kulunud aja, saab inimene teada teekonnal sinna ja tagasi kulunud aja. Lisades külalistelt lahkumise ajale poole teeloleku ajast, saab ta võimaluse teada saada koju jõudmise aeg ja selle järgi oma kella osutid reguleerida.

Leidke aeg, millal server päringuga töötas:

  1. Paketi reisiaja leidmine kliendilt serverisse: ((Saabumine – päritolu) – (Edasta – Vastuvõtmine)) / 2
  2. Leia erinevus kliendi ja serveri aja vahel:
    Vastu võtma – väljastama – ((saabuma – päritolu) – (edastama – vastu võtma)) / 2 =
    2 * Vastuvõtmine - 2 * Päritolu - Saabumine + Saabumine + Edastamine - Vastuvõtmine =
    Vastuvõtmine - päritolu - saabumine + edastamine

Lisame saadud väärtuse kohalikule ajale ja naudime elu.

Tulemuse väljund

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)

Kasulik link.

Allikas: www.habr.com

Lisa kommentaar