It skriuwen fan in ienfâldige NTP-kliïnt

Hallo, Habrausers. Hjoed wol ik prate oer hoe't jo jo eigen ienfâldige NTP-kliïnt skriuwe. Yn prinsipe sil it petear draaie nei de struktuer fan it pakket en de metoade foar it ferwurkjen fan it antwurd fan 'e NTP-tsjinner. De koade sil yn Python skreaun wurde, want it liket my dat der gewoan gjin bettere taal is foar soksoarte dingen. Kenners sille de oerienkomst fan 'e koade mei de ntplib-koade opmerke - ik waard der "ynspirearre" troch.

Dus wat is NTP krekt? NTP is in protokol foar ynteraksje mei eksakte tiidservers. Dit protokol wurdt brûkt yn in protte moderne masines. Bygelyks de w32tm-tsjinst yn finsters.

D'r binne yn totaal 5 ferzjes fan it NTP-protokol. De earste, ferzje 0 (1985, RFC958)), wurdt op it stuit beskôge as ferâldere. No wurde de nijere brûkt, 1e (1988, RFC1059), 2e (1989, RFC1119), 3e (1992, RFC1305) en 4e (1996, RFC2030). Ferzjes 1-4 binne kompatibel mei elkoar; se ferskille allinich yn 'e serveroperaasjealgoritmen.

Pakketformaat

It skriuwen fan in ienfâldige NTP-kliïnt

Leap indicator (korreksje indicator) - in getal oanjout in warskôging oer de koördinaasje twadde. Betsjutting:

  • 0 - gjin korreksje
  • 1 - de lêste minút fan 'e dei befettet 61 sekonden
  • 2 - de lêste minút fan 'e dei befettet 59 sekonden
  • 3 - tsjinner flater (tiid wurdt net syngronisearre)

Ferzje nûmer (ferzjenûmer) - NTP-protokolferzjenûmer (1-4).

fashion (modus) - operaasjemodus fan 'e pakketstjoerder. Wearde fan 0 oant 7, meast foarkommen:

  • 3 - kliïnt
  • 4 - tsjinner
  • 5 - útstjoermodus

stratus (laachnivo) - it oantal tuskenlagen tusken de tsjinner en de referinsjeklok (1 - de tsjinner nimt gegevens direkt fan 'e referinsjeklok, 2 - de tsjinner nimt gegevens fan in tsjinner mei laach 1, ensfh.).
pool is in ûndertekene hiel getal dat it maksimum ynterval tusken opfolgjende berjochten fertsjintwurdiget. De NTP-kliïnt spesifisearret hjir it ynterval wêryn't it ferwachtet om de tsjinner te ûndersiikjen, en de NTP-tsjinner spesifiseart it ynterval wêryn't it ferwachtet te wurde ûndersocht. De wearde is lyk oan de binêre logaritme fan sekonden.
Krektens (krektens) is in tekene hiel getal dat de krektens fan 'e systeemklok fertsjintwurdiget. De wearde is lyk oan de binêre logaritme fan sekonden.
Root fertraging (serverfertraging) - de tiid dy't it duorret foar de kloklêzingen om de NTP-tsjinner te berikken, as in fêst punt oantal sekonden.
Root dispersion (serversprieding) - fersprieding fan NTP-tsjinner kloklêzingen as in oantal sekonden mei in fêst punt.
Ref id (boarne identifier) ​​- klok id. As de tsjinner stratum 1 hat, dan is ref id de namme fan 'e atoomklok (4 ASCII-tekens). As de tsjinner in oare tsjinner brûkt, dan befettet de ref id it adres fan dizze tsjinner.
De lêste 4 fjilden fertsjintwurdigje de tiid - 32 bits - it hiele diel, 32 bits - it fraksjediel.
Referinsje - de lêste kloklêzingen op 'e tsjinner.
Originearje - tiid doe't it pakket waard ferstjoerd (ynfolle troch de tsjinner - mear oer dit hjirûnder).
Ûntfange - tiid dat it pakket waard ûntfongen troch de tsjinner.
transmit - tiid fan it ferstjoeren fan it pakket fan 'e tsjinner nei de kliïnt (ynfolle troch de kliïnt, mear oer dit hjirûnder).

Wy sille net beskôgje de lêste twa fjilden.

Litte wy ús pakket skriuwe:

Pakket koade

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 in pakket nei de tsjinner te stjoeren (en ûntfange), moatte wy it yn in byte-array kinne omsette.
Foar dizze (en omkearde) operaasje sille wy twa funksjes skriuwe - pack () en unpack ():

pack funksje

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

útpakke funksje

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

Foar luie minsken, as in applikaasje - koade dy't in pakket feroaret yn in prachtige snaar

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)

It ferstjoeren fan in pakket nei de tsjinner

In pakket mei ynfolle fjilden moat stjoerd wurde nei de tsjinner Ferzje, fashion и transmit. de transmit jo moatte opjaan de hjoeddeiske tiid op de lokale masine (it oantal sekonden sûnt 1. Jannewaris 1900), ferzje - ien fan 1-4, modus - 3 (client modus).

De tsjinner, nei it akseptearjen fan it fersyk, foltôget alle fjilden yn it NTP-pakket, kopiearret yn it fjild Originearje wearde fan transmit, dy't yn it fersyk kaam. It is my in riedsel wêrom't de kliïnt de wearde fan syn tiid op it fjild net daliks ynfolje kin Originearje. As gefolch, as it pakket weromkomt, hat de kliïnt 4 tiidwearden - de tiid dat it fersyk is ferstjoerd (Originearje), tiid dat de tsjinner it fersyk ûntfong (Ûntfange), tiid dat de tsjinner it antwurd stjoerde (transmit) en de tiid dat de klant it antwurd krige - Oankomme (net yn it pakket). Mei dizze wearden kinne wy ​​​​de juste tiid ynstelle.

Pakket ferstjoeren en ûntfange koade

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

It ferwurkjen fan gegevens fan de tsjinner

It ferwurkjen fan gegevens fan de server is gelyk oan de aksjes fan de Ingelske hear út it âlde probleem fan Raymond M. Smullyan (1978): “Ien man hie gjin polshorloazje, mar der wie in krekte muorreklok thús, dy’t er soms fergeat opwine. Op in dei, fergetten om syn horloazje wer te draaien, gong er nei syn freon, brocht de jûn by him troch, en doe't er thúskaam, wist er it horloazje goed yn te stellen. Hoe is it him slagge as de reistiid net fan tefoaren bekend wie? It antwurd is: "As jo ​​​​it hûs ferlitte, draait in persoan syn horloazje en ûnthâldt yn hokker posysje de hannen binne. Nei't er nei in freon kaam en de gasten ferliet, notearret hy de tiid fan syn oankomst en fertrek. Dêrmei kin er witte hoe lang er op besite west hat. Werom nei hûs en sjoch op 'e klok, in persoan bepaalt de doer fan syn ôfwêzigens. Troch fan dizze tiid de tiid dy't er besocht hat ôf te lûken, fynt in persoan de tiid dy't trochbrocht is oan it reizgjen en werom. Troch de helte fan 'e tiid dy't op 'e dyk bestege is ta te foegjen oan' e tiid fan it ferlitten fan 'e gasten, krijt hy de kâns om de tiid fan oankomst thús út te finen en de hannen fan syn horloazje dêrop oan te passen.

Fyn de tiid dat de tsjinner wurket oan in fersyk:

  1. Fyn de reistiid fan it pakket fan 'e kliïnt nei de tsjinner: ((oankomme - ûntsteane) - (ferstjoere - ûntfange)) / 2
  2. Fyn it ferskil tusken client- en servertiid:
    Untfange - Originearje - ((oankomme - Originearje) - (Transmit - Untfange)) / 2 =
    2 * Untfange - 2 * Untfange - Oankomme + Originearje + Ferstjoere - Untfange =
    Untfange - Originearje - Oankomme + Ferstjoere

Wy foegje de resultearjende wearde ta oan 'e lokale tiid en genietsje fan it libben.

Utfier fan it resultaat

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)

Brûkber link.

Boarne: www.habr.com

Add a comment