Yksinkertaisen NTP-asiakkaan kirjoittaminen

Hei, Habrausers. Tänään haluan puhua oman yksinkertaisen NTP-asiakkaan kirjoittamisesta. Pohjimmiltaan keskustelu siirtyy paketin rakenteeseen ja NTP-palvelimen vastauksen käsittelytapaan. Koodi kirjoitetaan Pythonilla, koska minusta tuntuu, että sellaisille asioille ei yksinkertaisesti ole parempaa kieltä. Asiantuntijat huomaavat koodin samankaltaisuuden ntplib-koodin kanssa - siitä "inspiroiduin".

Joten mikä NTP oikein on? NTP on protokolla vuorovaikutukseen tarkan ajan palvelimien kanssa. Tätä protokollaa käytetään monissa nykyaikaisissa koneissa. Esimerkiksi w32tm-palvelu Windowsissa.

NTP-protokollasta on yhteensä 5 versiota. Ensimmäinen, versio 0 (1985, RFC958)), katsotaan tällä hetkellä vanhentuneeksi. Nyt käytössä ovat uudemmat, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) ja 4. (1996, RFC2030). Versiot 1-4 ovat yhteensopivia keskenään, ne eroavat toisistaan ​​vain palvelimen toiminta-algoritmien osalta.

Paketin muoto

Yksinkertaisen NTP-asiakkaan kirjoittaminen

Hyppyn ilmaisin (korjausilmaisin) - numero, joka osoittaa varoituksen koordinaatiosekunnista. Merkitys:

  • 0 – ei korjausta
  • 1 – päivän viimeinen minuutti sisältää 61 sekuntia
  • 2 – päivän viimeinen minuutti sisältää 59 sekuntia
  • 3 – palvelimen toimintahäiriö (aikaa ei synkronoida)

Versionumero (versionumero) – NTP-protokollan versionumero (1-4).

tila (mode) — paketin lähettäjän toimintatila. Arvo 0-7, yleisin:

  • 3 – asiakas
  • 4 – palvelin
  • 5 – lähetystila

kerrostuma (kerrostaso) – palvelimen ja referenssikellon välisten välikerrosten lukumäärä (1 – palvelin ottaa tiedot suoraan referenssikellosta, 2 – palvelin ottaa tietoja palvelimelta, jolla on kerros 1 jne.).
pool on etumerkillinen kokonaisluku, joka edustaa peräkkäisten viestien enimmäisväliä. NTP-asiakas määrittää tässä aikavälin, jolla se odottaa kyselyn palvelimelta, ja NTP-palvelin määrittää aikavälin, jolla se odottaa kyselyn. Arvo on yhtä suuri kuin sekuntien binäärilogaritmi.
Tarkkuus (tarkkuus) on etumerkillinen kokonaisluku, joka edustaa järjestelmän kellon tarkkuutta. Arvo on yhtä suuri kuin sekuntien binäärilogaritmi.
Juuren viive (palvelimen viive) – aika, joka kuluu kellon lukemien saapumiseen NTP-palvelimelle, kiinteän pisteen sekuntimääränä.
Juuren dispersio (palvelimen leviäminen) - NTP-palvelimen kellon lukemien leviäminen sekuntimääränä kiinteällä pisteellä.
Viitetunnus (lähdetunniste) - kellon tunnus. Jos palvelimella on kerros 1, ref id on atomikellon nimi (4 ASCII-merkkiä). Jos palvelin käyttää toista palvelinta, ref id sisältää tämän palvelimen osoitteen.
Viimeiset 4 kenttää edustavat aikaa - 32 bittiä - kokonaislukuosaa, 32 bittiä - murto-osaa.
Viite — palvelimen viimeisimmät kellon lukemat.
Olla peräisin – aika, jolloin paketti lähetettiin (palvelimen täyttämä - tästä lisää alla).
Vastaanottaa – aika, jolloin palvelin vastaanotti paketin.
Lähettää – paketin lähetysaika palvelimelta asiakkaalle (asiakkaan täyttää, tästä lisää alla).

Emme ota huomioon kahta viimeistä kenttää.

Kirjoitetaanpa pakettimme:

Paketin koodi

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

Jotta voimme lähettää (ja vastaanottaa) paketin palvelimelle, meidän on kyettävä muuttamaan se tavutaulukoksi.
Tätä (ja käänteistä) toimintoa varten kirjoitamme kaksi funktiota - pack() ja unpack():

pakkaustoiminto

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

purkamistoiminto

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

Laiskoille, sovelluksena - koodi, joka muuttaa paketin kauniiksi merkkijonoksi

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)

Paketin lähettäminen palvelimelle

Täytetyt kentät sisältävä paketti on lähetettävä palvelimelle Versio, tila и Lähettää. Sisään Lähettää sinun on määritettävä nykyinen aika paikallisessa koneessa (sekuntien lukumäärä 1. tammikuuta 1900 lähtien), versio - mikä tahansa 1-4, tila - 3 (asiakastila).

Palvelin, hyväksyttyään pyynnön, täyttää kaikki NTP-paketin kentät ja kopioi kenttään Olla peräisin arvo alkaen Lähettää, joka tuli pyynnössä. Minulle on mysteeri, miksi asiakas ei voi heti täyttää kentällä aikansa arvoa Olla peräisin. Tämän seurauksena, kun paketti tulee takaisin, asiakkaalla on 4 aika-arvoa - pyynnön lähetysaika (Olla peräisin), kun palvelin vastaanotti pyynnön (Vastaanottaa), kun palvelin lähetti vastauksen (Lähettää) ja aika, jolloin asiakas sai vastauksen – Saapua (ei sisällä pakkauksessa). Näitä arvoja käyttämällä voimme asettaa oikean ajan.

Paketin lähetys- ja vastaanottokoodi

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

Käsittelee tietoja palvelimelta

Tietojen käsittely palvelimelta on samanlaista kuin englantilaisen herrasmiehen toiminta Raymond M. Smullyanin (1978) vanhasta ongelmasta: ”Yhdellä miehellä ei ollut rannekelloa, mutta kotona oli tarkka seinäkello, jonka hän joskus unohti. tuuleen. Eräänä päivänä, kun hän oli unohtanut kellonsa taas kelata, hän meni ystävänsä luokse, vietti illan hänen kanssaan, ja kotiin palattuaan hän onnistui asettamaan kellon oikein. Kuinka hän onnistui tekemään tämän, jos matka-aikaa ei tiedetty etukäteen? Vastaus kuuluu: ”Kotoa lähtiessään ihminen pyörittää kelloaan ja muistaa, missä asennossa osoittimet ovat. Tultuaan ystävän luo ja jättäessään vieraat hän merkitsee saapumis- ja lähtöaikansa. Näin hän saa selville, kuinka kauan hän oli vierailulla. Palattuaan kotiin ja katsomalla kelloa henkilö määrittää poissaolonsa keston. Vähentämällä tästä ajasta vieraillessaan vierailemaansa aikaa, henkilö saa selville sinne ja takaisin matkustamiseen kuluneen ajan. Lisäämällä puolet matkalla vieraiden lähtöaikaan, hän saa mahdollisuuden selvittää kotiintuloajan ja säätää kellonsa osoittimet sen mukaan.

Etsi aika, jolloin palvelin työskentelee pyynnön kanssa:

  1. Etsi paketin matka-aika asiakkaalta palvelimelle: ((Saapu – Lähde) – (Lähetä – Vastaanota)) / 2
  2. Selvitä asiakkaan ja palvelimen ajan välinen ero:
    Vastaanota - Lähde - ((Saapu - Lähde) - (Lähetä - Vastaanota)) / 2 =
    2 * Vastaanota – 2 * Lähde – Saapuminen + Lähde + Lähetä – Vastaanota =
    Vastaanota – Lähde – Saapuminen + Lähetä

Lisäämme syntyvän arvon paikalliseen aikaan ja nautimme elämästä.

Tuloksen tulos

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)

Hyödyllinen linkki.

Lähde: will.com

Lisää kommentti