Paprasto NTP kliento rašymas

Sveiki habrauseriai. Šiandien noriu pakalbėti apie tai, kaip parašyti savo paprastą NTP klientą. Iš esmės pokalbis bus nukreiptas į paketo struktūrą ir tai, kaip apdorojamas atsakymas iš NTP serverio. Kodas bus parašytas python, nes, mano nuomone, geresnės kalbos tokiems dalykams tiesiog nėra. Žinovai atkreips dėmesį į kodo panašumą su ntplib kodu – mane jis „įkvėpė“.

Taigi, kas tiksliai yra NTP? NTP yra sąveikos su tikslaus laiko serveriais protokolas. Šis protokolas naudojamas daugelyje šiuolaikinių mašinų. Pavyzdžiui, w32tm paslauga Windows.

Iš viso yra 5 NTP protokolo versijos. Pirmoji, 0 versija (1985, RFC958)), šiuo metu laikoma pasenusia. Šiuo metu naudojami naujesni, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) ir 4 (1996, RFC2030). 1-4 versijos yra suderinamos viena su kita, skiriasi tik serverių algoritmais.

Paketo formatas

Paprasto NTP kliento rašymas

Šuolio indikatorius (koregavimo indikatorius) yra skaičius, nurodantis sekundės perspėjimą. Reikšmė:

  • 0 – nėra pataisymų
  • 1 – paskutinė dienos minutė yra 61 sekundė
  • 2 – paskutinė dienos minutė yra 59 sekundės
  • 3 – serverio gedimas (laikas nesinchronizuojamas)

Versijos numeris (versijos numeris) – NTP protokolo versijos numeris (1-4).

Režimas (režimas) — paketų siuntėjo darbo režimas. Reikšmė nuo 0 iki 7, dažniausiai:

  • 3 – klientas
  • 4 – serveris
  • 5 – transliacijos režimas

sluoksnis (sluoksniavimo lygis) – tarpinių sluoksnių skaičius tarp serverio ir atskaitos laikrodžio (1 – serveris ima duomenis tiesiai iš atskaitos laikrodžio, 2 – serveris ima duomenis iš 1 lygio serverio ir t. t.).
Baseinas yra sveikasis ženklas su ženklu, nurodantis didžiausią intervalą tarp nuoseklių pranešimų. NTP klientas čia nurodo intervalą, per kurį jis tikisi apklausti serverį, o NTP serveris nurodo intervalą, kuriuo tikisi apklausa. Reikšmė lygi dvejetainiam sekundžių logaritmui.
Tikslumas (tikslumas) yra sveikasis skaičius, žymintis sistemos laikrodžio tikslumą. Reikšmė lygi dvejetainiam sekundžių logaritmui.
Šaknies vėlavimas (serverio delsa) yra laikas, per kurį laikrodis pasiekia NTP serverį, kaip fiksuotas sekundžių skaičius.
Šaknų dispersija (serverio sklaida) – NTP serverio laikrodžio sklaida kaip fiksuoto taško sekundžių skaičius.
Nuorodos id (šaltinio identifikatorius) - laikrodžio id. Jei serveris turi 1 sluoksnį, ref id yra atominio laikrodžio pavadinimas (4 ASCII simboliai). Jei serveris naudoja kitą serverį, ref id yra šio serverio adresas.
Paskutiniai 4 laukai yra laikas – 32 bitai – sveikoji dalis, 32 bitai – trupmeninė dalis.
Nuoroda — naujausi serverio laikrodžio rodmenys.
Kilmė – laikas, kada buvo išsiųstas paketas (užpildė serveris – daugiau apie tai žemiau).
Gauti – laikas, kada serveris gavo paketą.
Perduoti – laikas, kada paketas buvo išsiųstas iš serverio klientui (užpildo klientas, plačiau apie tai žemiau).

Pastarųjų dviejų laukų nenagrinėsime.

Parašykime savo paketą:

Paketo kodas

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

Norėdami siųsti (ir gauti) paketą į serverį, turime sugebėti paversti jį baitų masyvu.
Šiai (ir atvirkštinei) operacijai parašysime dvi funkcijas - pack() ir unpack():

pakuotės funkcija

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

išpakavimo funkcija

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

Tinginiams, kaip programa – kodas, kuris paketą paverčia gražia eilute

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)

Siunčiamas paketas į serverį

Į serverį turi būti išsiųstas paketas su užpildytais laukais versija, Režimas и Perduoti. Į Perduoti turite nurodyti dabartinį laiką vietiniame kompiuteryje (sekundžių skaičius nuo 1 m. sausio 1900 d.), versija - bet kuri iš 1-4, režimas - 3 (kliento režimas).

Serveris, priėmęs užklausą, užpildo visus NTP paketo laukus, nukopijuodamas į laukelį Kilmė vertė nuo Perduoti, kuris buvo pateiktas prašyme. Man yra paslaptis, kodėl klientas negali iš karto užpildyti savo laiko vertės lauke Kilmė. Dėl to, kai paketas grįžta, klientas turi 4 laiko reikšmes - užklausos išsiuntimo laiką (Kilmė), kai serveris gavo užklausą (Gauti), laikas, kai serveris išsiuntė atsakymą (Perduoti) ir laikas, kai klientas gavo atsakymą – Atvykti (ne pakuotėje). Naudodami šias vertes galime nustatyti teisingą laiką.

Siuntimo ir gavimo kodas

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

Duomenų apdorojimas iš serverio

Duomenų apdorojimas iš serverio panašus į anglų džentelmeno veiksmus iš senos Raymondo M. Smalliano (1978 m.) problemos: „Vienas žmogus neturėjo rankinio laikrodžio, bet namuose buvo tikslus sieninis laikrodis, kurį jis kartais pamiršdavo vėją. Vieną dieną, pamiršęs vėl įjungti laikrodį, nuėjo pas draugą, su juo pavakarojo, o grįžęs namo pavyko teisingai nustatyti laikrodį. Kaip jam tai pavyko padaryti, jei kelionės laikas nebuvo žinomas iš anksto? Atsakymas toks: „Išeidamas iš namų žmogus atsukinėja laikrodį ir prisimena rodyklių padėtį. Atėjęs pas draugą ir palikęs svečius, jis pažymi savo atvykimo ir išvykimo laiką. Tai leidžia jam sužinoti, kiek laiko jis lankėsi. Grįžęs namo ir žiūrėdamas į laikrodį žmogus nustato savo nebuvimo trukmę. Iš šio laiko atėmus laiką, kurį praleido lankydamasis, žmogus sužino laiką, praleistą kelyje ten ir atgal. Pusę kelyje praleisto laiko pridėjęs prie išvykimo iš svečių, jis įgyja galimybę sužinoti atvykimo laiką namo ir atitinkamai pakoreguoti savo laikrodžio rodykles.

Raskite laiką, kada serveris dirba pagal užklausą:

  1. Raskite paketo kelionės iš kliento į serverį laiką: ((Atvykti – kilti) – (Persiųsti – gauti)) / 2
  2. Raskite skirtumą tarp kliento ir serverio laiko:
    Gauti – kilti – ((atvykti – kilti) – (perduoti – gauti)) / 2 =
    2 * Gauti - 2 * Iškelti - atvykti + kilti + perduoti - gauti =
    Gauti – kilti – atvykti + perduoti

Gautą vertę pridedame prie vietos laiko ir mėgaujamės gyvenimu.

Rezultato išvedimas

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)

Naudinga nuoroda.

Šaltinis: www.habr.com

Добавить комментарий