Een eenvoudige NTP-client schrijven

Hallo habrausers. Vandaag wil ik het hebben over het schrijven van je eigen eenvoudige NTP-client. Kortom, het gesprek gaat over de structuur van het pakket en hoe het antwoord van de NTP-server wordt verwerkt. De code zal in python worden geschreven, omdat er naar mijn mening gewoon geen betere taal is voor zulke dingen. Kenners zullen letten op de gelijkenis van de code met de ntplib-code - ik was erdoor "geΓ―nspireerd".

Dus wat is NTP eigenlijk? NTP is een protocol voor communicatie met tijdservers. Dit protocol wordt gebruikt in veel moderne machines. Bijvoorbeeld de w32tm-service op Windows.

Er zijn in totaal 5 versies van het NTP-protocol. De eerste versie 0 (1985, RFC958) wordt momenteel als achterhaald beschouwd. Nieuwere worden momenteel gebruikt, 1e (1988, RFC1059), 2e (1989, RFC1119), 3e (1992, RFC1305) en 4e (1996, RFC2030). Versies 1-4 zijn compatibel met elkaar, ze verschillen alleen in de algoritmen van de servers.

Pakketformaat

Een eenvoudige NTP-client schrijven

Sprong indicator (correctie-indicator) is een getal dat de schrikkelsecondewaarschuwing aangeeft. Betekenis:

  • 0 - geen correctie
  • 1 - de laatste minuut van de dag bevat 61 seconden
  • 2 - de laatste minuut van de dag bevat 59 seconden
  • 3 - serverstoring (time out of sync)

Versienummer (versienummer) – NTP-protocol versienummer (1-4).

Mode (mode) β€” werkingsmodus van de verzender van het pakket. Waarde van 0 tot 7, meest voorkomend:

  • 3 - klant
  • 4 - serveerder
  • 5 - uitzendmodus

laag (laagniveau) - het aantal tussenliggende lagen tussen de server en de referentieklok (1 - de server haalt gegevens rechtstreeks van de referentieklok, 2 - de server haalt gegevens van de server met niveau 1, enz.).
Zwembad is een geheel getal met teken dat het maximale interval tussen opeenvolgende berichten vertegenwoordigt. De NTP-client specificeert hier het interval waarmee hij verwacht de server te pollen, en de NTP-server specificeert het interval waarmee hij verwacht te pollen. De waarde is gelijk aan de binaire logaritme van seconden.
precisie (precisie) is een geheel getal met teken dat de nauwkeurigheid van de systeemklok vertegenwoordigt. De waarde is gelijk aan de binaire logaritme van seconden.
root vertraging (serverlatentie) is de tijd die de klok nodig heeft om de NTP-server te bereiken, als een vast aantal seconden.
wortelverspreiding (serverspreiding) - De spreiding van de NTP-serverklok als een vast aantal seconden.
Ref-id (bron-ID) - kijk-ID. Als de server stratum 1 heeft, dan is ref id de naam van de atoomklok (4 ASCII-tekens). Als de server een andere server gebruikt, dan bevat de ref id het adres van deze server.
De laatste 4 velden zijn de tijd - 32 bits - het gehele deel, 32 bits - het fractionele deel.
Referentie - de laatste klok op de server.
Ontstaan – tijd waarop het pakket is verzonden (ingevuld door de server – daarover hieronder meer).
Ontvangen – tijd waarop het pakket door de server is ontvangen.
Overbrengen – tijd waarop het pakket van de server naar de client is verzonden (ingevuld door de client, daarover hieronder meer).

De laatste twee velden worden niet in aanmerking genomen.

Laten we ons pakket schrijven:

Pakketcode

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

Om een ​​pakket naar de server te sturen (en te ontvangen), moeten we het kunnen omzetten in een reeks bytes.
Voor deze (en omgekeerde) bewerking zullen we twee functies schrijven - pack() en unpack():

pak functie

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

functie uitpakken

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

Voor luie mensen, als applicatie - code die het pakket in een mooie string verandert

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)

Een pakket naar de server sturen

Stuur een pakket met gevulde velden naar de server Versie, Mode ΠΈ Overbrengen. In Overbrengen u moet de huidige tijd op de lokale computer specificeren (aantal seconden sinds 1 januari 1900), versie - een van 1-4, modus - 3 (clientmodus).

De server, die het verzoek heeft ontvangen, vult alle velden in het NTP-pakket in en kopieert het in het veld Ontstaan waarde vanaf Overbrengen, die in het verzoek kwam. Het is mij een raadsel waarom de opdrachtgever niet meteen de waarde van zijn tijd in het veld kan invullen Ontstaan. Als gevolg hiervan heeft de client, wanneer het pakket terugkomt, 4 tijdwaarden - de tijd waarop het verzoek is verzonden (Ontstaan), het tijdstip waarop de server het verzoek heeft ontvangen (Ontvangen), het tijdstip waarop de server het antwoord heeft verzonden (Overbrengen) en het tijdstip van ontvangst van een reactie van de klant - Aankomst (niet in het pakket). Met deze waarden kunnen we de juiste tijd instellen.

Pakket verzend- en ontvangstcode

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

Gegevensverwerking vanaf de server

De verwerking van gegevens van de server is vergelijkbaar met de handelingen van de Engelse heer uit het oude probleem van Raymond M. Smallian (1978): β€œOne person did not have a wristwatch, but there was a accurate wall clock at home, that he soms vergeten op te winden. Op een dag vergat hij de klok opnieuw te starten, ging hij op bezoek bij zijn vriend, bracht de avond met hem door en toen hij thuiskwam, slaagde hij erin de klok correct in te stellen. Hoe kreeg hij dat voor elkaar als de reistijd vooraf niet bekend was? Het antwoord is: β€œAls iemand het huis verlaat, windt hij de klok op en onthoudt hij de positie van de wijzers. Als hij naar een vriend komt en de gasten verlaat, noteert hij de tijd van zijn aankomst en vertrek. Zo kan hij achterhalen hoe lang hij weg was. Bij thuiskomst en kijkend naar de klok, bepaalt een persoon de duur van zijn afwezigheid. Door van deze tijd de tijd af te trekken die hij op bezoek heeft doorgebracht, komt de persoon erachter hoeveel tijd hij op de weg heen en terug heeft doorgebracht. Door de helft van de tijd die op de weg is doorgebracht op te tellen bij het tijdstip van vertrek van de gasten, krijgt hij de mogelijkheid om de tijd van thuiskomst te achterhalen en de wijzers van zijn klok daarop af te stellen.

Vind de tijd dat de server aan het verzoek werkte:

  1. De reistijd van het pakket van de client naar de server vinden: ((Aankomen - Ontstaan) - (Verzenden - Ontvangen)) / 2
  2. Zoek het verschil tussen client- en servertijd:
    Ontvangen - Afkomstig - ((Aankomen - Afkomstig) - (Verzenden - Ontvangen)) / 2 =
    2 * Ontvangen - 2 * Afkomstig - Aankomen + Afkomstig + Verzenden - Ontvangen =
    Ontvangen - Ontstaan ​​- Aankomen + Verzenden

We tellen de ontvangen waarde op bij de lokale tijd en genieten van het leven.

Resultaat uitvoer

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)

Bruikbaar link.

Bron: www.habr.com

Voeg een reactie