Egyszerű NTP kliens írása

Sziasztok habrauserek. Ma arról szeretnék beszélni, hogyan írhat saját egyszerű NTP-klienst. Alapvetően a beszélgetés a csomag szerkezetére és az NTP-kiszolgáló válaszának feldolgozására irányul. A kód pythonban lesz megírva, mert véleményem szerint egyszerűen nincs jobb nyelv az ilyesmire. Az ínyencek figyelni fognak a kód hasonlóságára az ntplib kóddal - engem ez "ihletett meg".

Szóval mi az az NTP? Az NTP egy protokoll az időkiszolgálókkal való kommunikációhoz. Ezt a protokollt sok modern gépen használják. Például a w32tm szolgáltatás Windowson.

Az NTP protokollnak összesen 5 verziója létezik. Az első, 0-s verzió (1985, RFC958) jelenleg elavultnak számít. Jelenleg újabbak használatosak, az 1. (1988, RFC1059), a 2. (1989, RFC1119), a 3. (1992, RFC1305) és a 4. (1996, RFC2030). Az 1-4 verziók kompatibilisek egymással, csak a szerverek algoritmusaiban térnek el egymástól.

Csomag formátum

Egyszerű NTP kliens írása

Ugrásjelző (korrekciójelző) egy szám, amely a szökőmásodperces figyelmeztetést jelzi. Jelentése:

  • 0 - nincs javítás
  • 1 - a nap utolsó perce 61 másodpercet tartalmaz
  • 2 - a nap utolsó perce 59 másodpercet tartalmaz
  • 3 – szerverhiba (időtúllépés a szinkronban)

Verziószám (verziószám) – NTP protokoll verziószám (1-4).

Mód (mód) — a csomagküldő működési módja. 0 és 7 közötti érték, a leggyakoribb:

  • 3 - ügyfél
  • 4 - szerver
  • 5 - műsorszórási mód

Réteg (rétegzettségi szint) - a köztes rétegek száma a szerver és a referencia óra között (1 - a szerver közvetlenül a referenciaórától veszi az adatokat, 2 - a szerver az 1-es szintű szervertől veszi az adatokat stb.).
Medence karbantartására egy előjeles egész szám, amely az egymást követő üzenetek közötti maximális intervallumot jelenti. Az NTP-kliens itt adja meg azt az intervallumot, amelyen belül várhatóan lekérdezi a kiszolgálót, az NTP-kiszolgáló pedig azt az intervallumot, amelyen belül a lekérdezést várja. Az érték megegyezik a másodpercek bináris logaritmusával.
Pontosság (precíziós) egy előjeles egész szám, amely a rendszeróra pontosságát jelzi. Az érték megegyezik a másodpercek bináris logaritmusával.
gyökér késleltetés (szerver késleltetése) az az idő, amely alatt az óra eléri az NTP-kiszolgálót, fixpontos másodpercszámként.
gyökér diszperzió (szerver szórása) – Az NTP szerver órájának szóródása fixpontos másodpercszámként.
Ref id (forrásazonosító) – óraazonosító. Ha a szerver 1. réteggel rendelkezik, akkor a ref id az atomóra neve (4 ASCII karakter). Ha a szerver másik szervert használ, akkor a ref id ennek a szervernek a címét tartalmazza.
Az utolsó 4 mező az idő - 32 bit - az egész rész, 32 bit - a tört rész.
Referencia - a szerver legújabb órája.
Származik – a csomag elküldésének időpontja (a szerver kitölti – erről bővebben lent).
Kap – az idő, amikor a csomagot a szerver megkapta.
továbbít – a csomag elküldésének időpontja a szerverről a kliensnek (a kliens tölti ki, erről bővebben lentebb).

Az utolsó két mezőt nem vesszük figyelembe.

Írjuk meg a csomagunkat:

Csomag kódja

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

Ahhoz, hogy egy csomagot küldhessünk (és fogadhassunk) a szervernek, tudnunk kell azt bájtok tömbjévé alakítani.
Ehhez (és fordított) művelethez két függvényt írunk - pack() és unpack():

csomag funkció

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

kicsomagolás funkció

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

Lusta embereknek, mint alkalmazás - kód, amely a csomagot gyönyörű sztringgé alakítja

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)

Csomag küldése a szervernek

Csomag küldése kitöltött mezőkkel a szervernek Változat, Mód и továbbít. -Ban továbbít meg kell adni az aktuális időt a helyi gépen (1. január 1900-je óta eltelt másodpercek száma), verzió - 1-4, mód - 3 (kliens mód).

A szerver, miután megkapta a kérést, kitölti az NTP-csomag összes mezőjét, bemásolva a mezőbe Származik értéktől továbbít, ami a kérésben érkezett. Rejtély számomra, hogy az ügyfél miért nem tudja azonnal kitölteni a terepen eltöltött idejét Származik. Ennek eredményeként, amikor a csomag visszajön, az ügyfélnek 4 időértéke van - a kérés elküldésének időpontja (Származik), amikor a szerver megkapta a kérést (Kap), amikor a szerver elküldte a választ (továbbít) és a válasz ügyfél általi kézhezvételének időpontja - Megérkezik (nincs a csomagban). Ezekkel az értékekkel tudjuk beállítani a megfelelő időt.

Csomagküldési és -fogadási kód

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

Adatfeldolgozás a szerverről

A szerverről érkező adatok feldolgozása hasonló az angol úriember cselekedeteihez Raymond M. Smallian (1978) régi problémájából: „Egy embernek nem volt karórája, de volt otthon egy pontos falióra, amit ő néha elfelejtett szelelni. Egyik nap, amikor elfelejtette újra indítani az órát, elment meglátogatni barátját, vele töltötte az estét, és hazatérve sikerült helyesen beállítania az órát. Hogy sikerült ezt megtennie, ha nem volt előre ismert az utazási idő? A válasz: „A házat elhagyva az ember feltekeri az órát, és emlékszik a mutatók helyzetére. Egy barátjához érve és a vendégeket elhagyva feljegyzi érkezésének és távozásának idejét. Ez lehetővé teszi számára, hogy megtudja, mennyi ideig volt távol. Hazatérve és az órára nézve az ember meghatározza távollétének időtartamát. Ebből az időből levonva a látogatással töltött időt, a személy megtudja az oda-vissza úton töltött időt. Az úton töltött idő felét hozzáadva a vendégek távozásának idejéhez, lehetőséget kap arra, hogy megtudja a hazaérkezés idejét, és ennek megfelelően állítsa be órája mutatóit.

Keresse meg az időpontot, amikor a szerver dolgozott a kéréssel:

  1. A csomagok utazási idejének megkeresése a klienstől a szerverig: ((Érkezés - Eredet) - (Küldés - Fogadás)) / 2
  2. Keresse meg a különbséget a kliens és a szerver ideje között:
    Fogadás - Eredet - ((Érkezés - Eredet) - (Küldés - Fogadás)) / 2 =
    2 * Fogadás - 2 * Eredet - Érkezés + Eredmény + Küldés - Fogadás =
    Fogadás - Eredet - Érkezés + Küldés

A kapott értéket hozzáadjuk a helyi időhöz, és élvezzük az életet.

Eredménykimenet

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)

Hasznos link.

Forrás: will.com

Hozzászólás