Shkrimi i një klienti të thjeshtë NTP

Përshëndetje habrausers. Sot dua të flas se si të shkruani klientin tuaj të thjeshtë NTP. Në thelb, biseda do të kthehet në strukturën e paketës dhe mënyrën se si përpunohet përgjigja nga serveri NTP. Kodi do të shkruhet në python, sepse, për mendimin tim, thjesht nuk ka gjuhë më të mirë për gjëra të tilla. Njohësit do t'i kushtojnë vëmendje ngjashmërisë së kodit me kodin ntplib - unë u "frymëzova" prej tij.

Pra, çfarë është gjithsesi NTP? NTP është një protokoll për komunikimin me serverët e kohës. Ky protokoll përdoret në shumë makina moderne. Për shembull, shërbimi w32tm në Windows.

Gjithsej janë 5 versione të protokollit NTP. I pari, versioni 0 (1985, RFC958) aktualisht konsiderohet i vjetëruar. Aktualisht përdoren më të rejat, i pari (1, RFC1988), i dyti (1059, RFC2), i treti (1989, RFC1119) dhe i katërti (3, RFC1992). Versionet 1305-4 janë të pajtueshëm me njëri-tjetrin, ato ndryshojnë vetëm në algoritmet e serverëve.

Formati i paketës

Shkrimi i një klienti të thjeshtë NTP

Treguesi i kërcimit (treguesi i korrigjimit) është një numër që tregon paralajmërimin e dytë të brishtë. Kuptimi:

  • 0 - pa korrigjim
  • 1 - minuta e fundit e ditës përmban 61 sekonda
  • 2 - minuta e fundit e ditës përmban 59 sekonda
  • 3 - dështimi i serverit (koha jashtë sinkronizimit)

Numri i versionit (numri i versionit) – Numri i versionit të protokollit NTP (1-4).

mënyrë (mode) - mënyra e funksionimit të dërguesit të paketave. Vlera nga 0 në 7, më e zakonshme:

  • 3 - klient
  • 4 - server
  • 5 - mënyra e transmetimit

Shtresë (niveli i shtresave) - numri i shtresave të ndërmjetme midis serverit dhe orës referuese (1 - serveri merr të dhëna direkt nga ora e referencës, 2 - serveri merr të dhëna nga serveri me nivelin 1, etj.).
pishinë është një numër i plotë i nënshkruar që përfaqëson intervalin maksimal midis mesazheve të njëpasnjëshme. Klienti NTP specifikon këtu intervalin në të cilin pret të anketojë serverin dhe serveri NTP specifikon intervalin në të cilin pret të anketohet. Vlera është e barabartë me logaritmin binar të sekondave.
Saktësi (precision) është një numër i plotë i nënshkruar që përfaqëson saktësinë e orës së sistemit. Vlera është e barabartë me logaritmin binar të sekondave.
vonesë rrënjësore (latenca e serverit) është koha që i duhet orës për të arritur në serverin NTP, si një numër sekondash me pikë fikse.
dispersioni i rrënjës (shpërndarja e serverit) - Shpërndarja e orës së serverit NTP si një numër sekondash me pikë fikse.
Ref id (id i burimit) – watch id. Nëse serveri ka shtresën 1, atëherë ref id është emri i orës atomike (4 karaktere ASCII). Nëse serveri përdor një server tjetër, atëherë id ref përmban adresën e këtij serveri.
4 fushat e fundit janë koha - 32 bit - pjesa e plotë, 32 bit - pjesa e pjesshme.
referim - ora më e fundit në server.
Origjina – koha kur u dërgua paketa (e plotësuar nga serveri – më shumë për atë më poshtë).
marr – koha kur paketa është marrë nga serveri.
Transmetoj – koha kur paketa është dërguar nga serveri te klienti (plotësuar nga klienti, më shumë për këtë më poshtë).

Dy fushat e fundit nuk do të merren parasysh.

Le të shkruajmë paketën tonë:

Kodi i paketës

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

Për të dërguar (dhe marrë) një paketë në server, ne duhet të jemi në gjendje ta kthejmë atë në një grup bajtësh.
Për këtë operacion (dhe të kundërt), ne do të shkruajmë dy funksione - pack() dhe unpack():

funksioni i paketës

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

funksioni i shpaketimit

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

Për dembelët, si një aplikacion - kod që e kthen paketën në një varg të bukur

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)

Dërgimi i një pakete në server

Dërgoni një paketë me fushat e mbushura në server Version, mënyrë и Transmetoj. Në Transmetoj duhet të specifikoni kohën aktuale në makinën lokale (numri i sekondave që nga 1 janari 1900), versioni - cilido nga 1-4, modaliteti - 3 (modaliteti i klientit).

Serveri, pasi ka marrë kërkesën, plotëson të gjitha fushat në paketën NTP, duke kopjuar në fushë Origjina vlera nga Transmetoj, e cila erdhi në kërkesë. Është një mister për mua pse klienti nuk mund të plotësojë menjëherë vlerën e kohës së tij në fushë Origjina. Si rezultat, kur paketa kthehet, klienti ka 4 vlera kohore - koha kur është dërguar kërkesa (Origjina), koha kur serveri mori kërkesën (marr), koha kur serveri dërgoi përgjigjen (Transmetoj) dhe koha e marrjes së një përgjigje nga klienti - arrij (jo në paketë). Me këto vlera mund të vendosim kohën e duhur.

Kodi i dërgimit dhe marrjes së paketës

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

Përpunimi i të dhënave nga serveri

Përpunimi i të dhënave nga serveri është i ngjashëm me veprimet e zotërisë angleze nga problemi i vjetër i Raymond M. Smallian (1978): “Një person nuk kishte një orë dore, por në shtëpi kishte një orë muri të saktë, të cilën ai ndonjëherë harronte të frynte. Një ditë, duke harruar të rifillojë orën, ai shkoi për të vizituar shokun e tij, e kaloi mbrëmjen me të dhe kur u kthye në shtëpi, arriti ta caktojë saktë orën. Si arriti ta bënte këtë nëse nuk dihej paraprakisht koha e udhëtimit? Përgjigja është: “Duke larguar nga shtëpia, një person mbyll orën dhe kujton pozicionin e akrepave. Duke ardhur te një mik dhe duke u larguar nga të ftuarit, ai shënon kohën e mbërritjes dhe nisjes së tij. Kjo i lejon atij të zbulojë se sa kohë ishte larg. Duke u kthyer në shtëpi dhe duke parë orën, një person përcakton kohëzgjatjen e mungesës së tij. Duke zbritur nga kjo kohë kohën që ai kaloi duke vizituar, personi zbulon kohën e kaluar në rrugën atje dhe mbrapa. Duke shtuar gjysmën e kohës së kaluar në rrugë në kohën e largimit nga të ftuarit, ai merr mundësinë të zbulojë kohën e mbërritjes në shtëpi dhe të rregullojë akrepat e orës së tij në përputhje me rrethanat.

Gjeni kohën kur serveri po punonte me kërkesën:

  1. Gjetja e kohës së udhëtimit të paketës nga klienti në server: ((Mbërritja - Origjina) - (Transmetoni - Merr)) / 2
  2. Gjeni ndryshimin midis kohës së klientit dhe serverit:
    Merr - Origjinon - ((Mbërri - Origjinon) - (Transmet - Merr)) / 2 =
    2 * Merr - 2 * Origjina - Arri + Origjina + Transmeto - Merr =
    Merr - Origjinon - Arri + Transmeto

Ne ia shtojmë vlerën e marrë kohës lokale dhe shijojmë jetën.

Prodhimi i rezultatit

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)

E dobishme lidhje.

Burimi: www.habr.com

Shto një koment