Vienkārša NTP klienta rakstīšana

Sveiki habrauzeri. Šodien es vēlos runāt par to, kā uzrakstīt savu vienkāršo NTP klientu. Būtībā saruna pievērsīsies paketes struktūrai un tam, kā tiek apstrādāta atbilde no NTP servera. Kods tiks rakstīts python, jo, manuprāt, labākas valodas tādām lietām vienkārši nav. Zinātāji pievērsīs uzmanību koda līdzībai ar ntplib kodu - mani tas "iedvesmoja".

Tātad, kas vispār ir NTP? NTP ir protokols saziņai ar laika serveriem. Šis protokols tiek izmantots daudzās modernās iekārtās. Piemēram, w32tm pakalpojums Windows.

Kopumā pastāv 5 NTP protokola versijas. Pirmā, 0. versija (1985, RFC958), pašlaik tiek uzskatīta par novecojušu. Pašlaik tiek izmantotas jaunākas versijas: 1. versija (1988, RFC1059), 2. versija (1989, RFC1119), 3. versija (1992, RFC1305) un 4. versija (1996, RFC2030). 1.–4. versija ir savstarpēji saderīgas; tās atšķiras tikai ar darbības algoritmiem. serveriem.

Pakešu formāts

Vienkārša NTP klienta rakstīšana

Lēciena indikators (korekcijas indikators) ir skaitlis, kas norāda lēciena sekundes brīdinājumu. Nozīme:

  • 0 - nav labojumu
  • 1 - dienas pēdējā minūte satur 61 sekundi
  • 2 - dienas pēdējā minūte satur 59 sekundes
  • 3 — servera kļūme (nesinhronizācijas laiks)

Versijas numurs (versijas numurs) – NTP protokola versijas numurs (1-4).

Mode (režīms) — pakešu sūtītāja darbības režīms. Vērtība no 0 līdz 7, visbiežāk:

  • 3 - klients
  • 4 - serveris
  • 5 - apraides režīms

slānis (slāņa līmenis) - starpslāņu skaits starp serveri un atsauces pulksteni (1 - serveris ņem datus tieši no atsauces pulksteņa, 2 - serveris ņem datus no servera ar 1. līmeni utt.).
baseins ir vesels skaitlis ar zīmi, kas apzīmē maksimālo intervālu starp secīgiem ziņojumiem. NTP klients šeit norāda intervālu, pēc kura tas paredz veikt aptauju par serveri, un NTP serveris norāda intervālu, kurā tas paredz aptaujas veikšanu. Vērtība ir vienāda ar sekunžu bināro logaritmu.
Precizitāte (precizitāte) ir vesels skaitlis ar zīmi, kas atspoguļo sistēmas pulksteņa precizitāti. Vērtība ir vienāda ar sekunžu bināro logaritmu.
saknes kavēšanās (servera latentums) ir laiks, kas nepieciešams, lai pulkstenis sasniegtu NTP serveri, kā fiksēts sekunžu skaits.
sakņu dispersija (servera izkliede) — NTP servera pulksteņa izkliede kā fiksēta punkta sekunžu skaits.
Ref id (avota id) - skatīšanās ID. Ja serverim ir stratum 1, tad ref id ir atompulksteņa nosaukums (4 ASCII rakstzīmes). Ja serveris izmanto citu serveri, tad ref id satur šī servera adresi.
Pēdējie 4 lauki ir laiks - 32 biti - veselā skaitļa daļa, 32 biti - daļēja daļa.
atsauces - jaunākais pulkstenis serverī.
Izcelsme – laiks, kad pakete tika nosūtīta (aizpilda serveris – vairāk par to zemāk).
Saņemt – laiks, kad serveris saņēma paketi.
Pārraidīt – laiks, kad pakete tika nosūtīta no servera klientam (aizpilda klients, vairāk par to zemāk).

Pēdējie divi lauki netiks ņemti vērā.

Uzrakstīsim savu paketi:

Pakas kods

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

Lai nosūtītu (un saņemtu) paketi uz serveri, mums ir jāspēj to pārvērst baitu masīvā.
Šai (un apgrieztajai) darbībai mēs rakstīsim divas funkcijas - pack () un unpack ():

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

izpakošanas 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

Slinkiem cilvēkiem kā aplikācija - kods, kas iepakojumu pārvērš skaistā virknē

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)

Pakas nosūtīšana uz serveri

Nosūtiet uz serveri paketi ar aizpildītiem laukiem versija, Mode и Pārraidīt. Uz Pārraidīt jānorāda pašreizējais laiks vietējā mašīnā (sekunžu skaits kopš 1. gada 1900. janvāra), versija - jebkura no 1-4, režīms - 3 (klienta režīms).

Serveris, saņēmis pieprasījumu, aizpilda visus laukus NTP paketē, iekopējot laukā Izcelsme vērtība no Pārraidīt, kas tika iekļauts pieprasījumā. Man ir noslēpums, kāpēc klients nevar uzreiz aizpildīt laukā pavadītā laika vērtību Izcelsme. Tā rezultātā, kad pakete atgriežas, klientam ir 4 laika vērtības - pieprasījuma nosūtīšanas laiks (Izcelsme), laiks, kad serveris saņēma pieprasījumu (Saņemt), laiks, kad serveris nosūtīja atbildi (Pārraidīt) un laiks, kad klients ir saņēmis atbildi - Ierašanās (nav iepakojumā). Izmantojot šīs vērtības, mēs varam iestatīt pareizo laiku.

Pakas nosūtīšanas un saņemšanas kods

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

Datu apstrāde no servera

Datu apstrāde no servera ir līdzīga angļu džentlmeņa darbībām no Reimonda M. Smallian (1978) vecās problēmas: “Vienam cilvēkam nebija rokas pulksteņa, bet mājās bija precīzs sienas pulkstenis, kuru viņš dažreiz aizmirsa vēju. Kādu dienu, aizmirsis atkal iedarbināt pulksteni, viņš devās ciemos pie sava drauga, pavadīja ar viņu vakaru, un, atgriezies mājās, viņam izdevās pareizi iestatīt pulksteni. Kā viņam tas izdevās, ja ceļojuma laiks nebija iepriekš zināms? Atbilde ir: “Izejot no mājas, cilvēks uzgriež pulksteni un atceras rādījumu stāvokli. Atnākot pie drauga un atstājot viesus, viņš atzīmē ierašanās un aizbraukšanas laiku. Tas viņam ļauj uzzināt, cik ilgi viņš bija prom. Atgriežoties mājās un skatoties pulkstenī, cilvēks nosaka savas prombūtnes ilgumu. Atņemot no šī laika apmeklējuma laiku, cilvēks uzzina laiku, kas pavadīts ceļā uz turieni un atpakaļ. Pieskaitot pusi ceļā pavadītā laika viesu atstāšanas laikam, viņš iegūst iespēju uzzināt ierašanās laiku mājās un attiecīgi pielāgot sava pulksteņa rādītājus.

Atrodiet laiku, kad serveris strādāja ar pieprasījumu:

  1. Pakešu ceļojuma laika atrašana no klienta uz serveri: ((Ierodas - Izcelsme) - (Nosūtīt - Saņemt)) / 2
  2. Atrodiet atšķirību starp klienta un servera laiku:
    Saņemt - Izcelsme - ((Ierodas - Izcelsme) - (Nosūtīt - Saņemt)) / 2 =
    2 * Saņemt - 2 * Izcelsme - Ierašanās + Izcelsme + Pārsūtīt - Saņemt =
    Saņemt - Izcelsme - Pienākt + Pārsūtīt

Saņemto vērtību pievienojam vietējam laikam un baudām dzīvi.

Rezultātu izvade

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)

Noderīga saite.

Avots: www.habr.com

Pievieno komentāru