Γράψιμο ενός απλού προγράμματος-πελάτη NTP

Γεια σας habrausers. Σήμερα θέλω να μιλήσω για το πώς να γράψετε το δικό σας απλό πρόγραμμα-πελάτη NTP. Βασικά, η συνομιλία θα στραφεί στη δομή του πακέτου και στον τρόπο επεξεργασίας της απόκρισης από τον διακομιστή NTP. Ο κώδικας θα είναι γραμμένος σε python, γιατί, κατά τη γνώμη μου, απλά δεν υπάρχει καλύτερη γλώσσα για τέτοια πράγματα. Οι γνώστες θα δώσουν προσοχή στην ομοιότητα του κώδικα με τον κώδικα ntplib - «εμπνεύστηκα» από αυτόν.

Τι είναι λοιπόν το NTP; Το NTP είναι ένα πρωτόκολλο για την επικοινωνία με διακομιστές χρόνου. Αυτό το πρωτόκολλο χρησιμοποιείται σε πολλά σύγχρονα μηχανήματα. Για παράδειγμα, η υπηρεσία w32tm στα Windows.

Υπάρχουν συνολικά 5 εκδόσεις του πρωτοκόλλου NTP. Η πρώτη, έκδοση 0 (1985, RFC958) θεωρείται επί του παρόντος ξεπερασμένη. Αυτή τη στιγμή χρησιμοποιούνται νεότερα, 1η (1988, RFC1059), 2η (1989, RFC1119), 3η (1992, RFC1305) και 4η (1996, RFC2030). Οι εκδόσεις 1-4 είναι συμβατές μεταξύ τους, διαφέρουν μόνο στους αλγόριθμους των διακομιστών.

Μορφή πακέτου

Γράψιμο ενός απλού προγράμματος-πελάτη NTP

Ένδειξη άλματος (δείκτης διόρθωσης) είναι ένας αριθμός που υποδεικνύει την προειδοποίηση του δευτερολέπτου. Εννοια:

  • 0 - καμία διόρθωση
  • 1 - το τελευταίο λεπτό της ημέρας περιέχει 61 δευτερόλεπτα
  • 2 - το τελευταίο λεπτό της ημέρας περιέχει 59 δευτερόλεπτα
  • 3 - Αποτυχία διακομιστή (χρόνος εκτός συγχρονισμού)

Αριθμός έκδοσης (αριθμός έκδοσης) – αριθμός έκδοσης πρωτοκόλλου NTP (1-4).

Τρόπος (λειτουργία) — τρόπος λειτουργίας του αποστολέα πακέτων. Τιμή από 0 έως 7, πιο συνηθισμένη:

  • 3 - πελάτης
  • 4 - διακομιστής
  • 5 - λειτουργία εκπομπής

στρώμα (επίπεδο στρώσης) - ο αριθμός των ενδιάμεσων επιπέδων μεταξύ του διακομιστή και του ρολογιού αναφοράς (1 - ο διακομιστής λαμβάνει δεδομένα απευθείας από το ρολόι αναφοράς, 2 - ο διακομιστής παίρνει δεδομένα από τον διακομιστή με επίπεδο 1 κ.λπ.).
Πισίνα είναι ένας υπογεγραμμένος ακέραιος αριθμός που αντιπροσωπεύει το μέγιστο διάστημα μεταξύ διαδοχικών μηνυμάτων. Ο πελάτης NTP καθορίζει εδώ το διάστημα στο οποίο αναμένει να πραγματοποιήσει δημοσκόπηση στον διακομιστή και ο διακομιστής NTP καθορίζει το διάστημα στο οποίο αναμένει να γίνει δημοσκόπηση. Η τιμή είναι ίση με τον δυαδικό λογάριθμο των δευτερολέπτων.
Ακρίβεια (ακρίβεια) είναι ένας υπογεγραμμένος ακέραιος αριθμός που αντιπροσωπεύει την ακρίβεια του ρολογιού του συστήματος. Η τιμή είναι ίση με τον δυαδικό λογάριθμο των δευτερολέπτων.
καθυστέρηση ρίζας (λανθάνουσα κατάσταση διακομιστή) είναι ο χρόνος που χρειάζεται για να φτάσει το ρολόι στον διακομιστή NTP, ως σταθερός αριθμός δευτερολέπτων.
διασπορά ρίζας (διασκορπισμός διακομιστή) - Η διασπορά του ρολογιού διακομιστή NTP ως αριθμός δευτερολέπτων σταθερού σημείου.
Αναφ (αναγνωριστικό πηγής) – αναγνωριστικό ρολογιού. Εάν ο διακομιστής έχει στρώμα 1, τότε το ref id είναι το όνομα του ατομικού ρολογιού (4 χαρακτήρες ASCII). Εάν ο διακομιστής χρησιμοποιεί άλλο διακομιστή, τότε το ref id περιέχει τη διεύθυνση αυτού του διακομιστή.
Τα τελευταία 4 πεδία είναι ο χρόνος - 32 bit - το ακέραιο μέρος, 32 bit - το κλασματικό μέρος.
Αναφορά - το πιο πρόσφατο ρολόι στον διακομιστή.
Καταγομαι απο – ώρα αποστολής του πακέτου (συμπληρώθηκε από τον διακομιστή – περισσότερα για αυτό παρακάτω).
Λαμβάνω – ώρα λήψης του πακέτου από τον διακομιστή.
Μετάδοση – ώρα αποστολής του πακέτου από τον διακομιστή στον πελάτη (συμπληρώνεται από τον πελάτη, περισσότερα για αυτό παρακάτω).

Τα δύο τελευταία πεδία δεν θα ληφθούν υπόψη.

Ας γράψουμε το πακέτο μας:

Κωδικός πακέτου

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

Για να στείλουμε (και να λάβουμε) ένα πακέτο στον διακομιστή, πρέπει να μπορούμε να το μετατρέψουμε σε μια συστοιχία byte.
Για αυτήν την (και αντίστροφη) λειτουργία, θα γράψουμε δύο συναρτήσεις - pack() και unpack():

λειτουργία πακέτου

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

λειτουργία αποσυσκευασίας

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

Για τεμπέληδες, ως εφαρμογή - κωδικός που μετατρέπει το πακέτο σε ένα όμορφο string

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)

Αποστολή πακέτου στον διακομιστή

Στείλτε ένα πακέτο με συμπληρωμένα πεδία στον διακομιστή Εκδοχή, Τρόπος и Μετάδοση. Σε Μετάδοση πρέπει να καθορίσετε την τρέχουσα ώρα στο τοπικό μηχάνημα (αριθμός δευτερολέπτων από την 1η Ιανουαρίου 1900), έκδοση - οποιαδήποτε από τις 1-4, λειτουργία - 3 (λειτουργία πελάτη).

Ο διακομιστής, έχοντας λάβει το αίτημα, συμπληρώνει όλα τα πεδία στο πακέτο NTP, αντιγράφοντας στο πεδίο Καταγομαι απο αξία από Μετάδοση, που μπήκε στο αίτημα. Είναι ένα μυστήριο για μένα γιατί ο πελάτης δεν μπορεί να συμπληρώσει αμέσως την αξία του χρόνου του στο πεδίο Καταγομαι απο. Ως αποτέλεσμα, όταν το πακέτο επιστρέφει, ο πελάτης έχει 4 χρονικές τιμές - τη στιγμή που στάλθηκε το αίτημα (Καταγομαι απο), τη στιγμή που ο διακομιστής έλαβε το αίτημα (Λαμβάνω), την ώρα που ο διακομιστής έστειλε την απάντηση (Μετάδοση) και την ώρα λήψης απάντησης από τον πελάτη - Φθάνω (όχι στη συσκευασία). Με αυτές τις τιμές μπορούμε να ορίσουμε τη σωστή ώρα.

Κωδικός αποστολής και λήψης πακέτου

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

Επεξεργασία δεδομένων από τον διακομιστή

Η επεξεργασία των δεδομένων από τον διακομιστή είναι παρόμοια με τις ενέργειες του Άγγλου κυρίου από το παλιό πρόβλημα του Raymond M. Smallian (1978): «Ένα άτομο δεν είχε ρολόι χειρός, αλλά υπήρχε ένα ακριβές ρολόι τοίχου στο σπίτι, το οποίο μερικές φορές ξεχνούσε να κουρδίζει. Μια μέρα, ξεχνώντας να ξαναρχίσει το ρολόι, πήγε να επισκεφτεί τον φίλο του, πέρασε το βράδυ μαζί του και όταν επέστρεψε σπίτι, κατάφερε να ρυθμίσει σωστά το ρολόι. Πώς τα κατάφερε αν δεν ήταν εκ των προτέρων γνωστός ο χρόνος του ταξιδιού; Η απάντηση είναι: «Φεύγοντας από το σπίτι, ένα άτομο κλείνει το ρολόι και θυμάται τη θέση των χεριών. Ερχόμενος σε έναν φίλο και φεύγοντας από τους καλεσμένους, σημειώνει την ώρα της άφιξης και της αναχώρησής του. Αυτό του επιτρέπει να μάθει πόσο καιρό έλειπε. Επιστρέφοντας σπίτι και κοιτάζοντας το ρολόι, ένα άτομο καθορίζει τη διάρκεια της απουσίας του. Αφαιρώντας από αυτόν τον χρόνο τον χρόνο που πέρασε επισκεπτόμενος, το άτομο ανακαλύπτει τον χρόνο που πέρασε στο δρόμο εκεί και πίσω. Προσθέτοντας τον μισό χρόνο που αφιερώνει στο δρόμο στον χρόνο αποχώρησης των καλεσμένων, έχει την ευκαιρία να μάθει την ώρα άφιξης στο σπίτι και να προσαρμόσει ανάλογα τους δείκτες του ρολογιού του.

Βρείτε την ώρα που ο διακομιστής δούλευε στο αίτημα:

  1. Εύρεση του χρόνου ταξιδιού του πακέτου από τον πελάτη στον διακομιστή: ((Άφιξη - Προέλευση) - (Μετάδοση - Λήψη)) / 2
  2. Βρείτε τη διαφορά μεταξύ του χρόνου πελάτη και διακομιστή:
    Receive - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 2 =
    2 * Λήψη - 2 * Προέλευση - Άφιξη + Προέλευση + Μετάδοση - Λήψη =
    Λήψη - Προέλευση - Άφιξη + Μετάδοση

Προσθέτουμε τη λαμβανόμενη αξία στην τοπική ώρα και απολαμβάνουμε τη ζωή.

Έξοδος αποτελέσματος

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)

Χρήσιμος σύνδεσμος.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο