Menulis klien NTP yang mudah

Hello habrausers. Hari ini saya ingin bercakap tentang cara menulis klien NTP mudah anda sendiri. Pada asasnya, perbualan akan beralih kepada struktur paket dan bagaimana tindak balas daripada pelayan NTP diproses. Kod itu akan ditulis dalam python, kerana, pada pendapat saya, tidak ada bahasa yang lebih baik untuk perkara sedemikian. Para ahli akan memberi perhatian kepada persamaan kod dengan kod ntplib - Saya "diilhamkan" olehnya.

Jadi apakah NTP itu? NTP ialah protokol untuk berkomunikasi dengan pelayan masa. Protokol ini digunakan dalam banyak mesin moden. Contohnya, perkhidmatan w32tm pada windows.

Terdapat 5 versi protokol NTP secara keseluruhan. Yang pertama, versi 0 (1985, RFC958) kini dianggap usang. Yang lebih baru sedang digunakan, 1 (1988, RFC1059), 2nd (1989, RFC1119), ke-3 (1992, RFC1305) dan ke-4 (1996, RFC2030). Versi 1-4 adalah serasi antara satu sama lain, mereka hanya berbeza dalam algoritma pelayan.

Format Paket

Menulis klien NTP yang mudah

Penunjuk lompatan (penunjuk pembetulan) ialah nombor yang menunjukkan amaran lompatan kedua. Maksud:

  • 0 - tiada pembetulan
  • 1 - minit terakhir hari mengandungi 61 saat
  • 2 - minit terakhir hari mengandungi 59 saat
  • 3 - kegagalan pelayan (masa tidak segerak)

Nombor versi (nombor versi) – Nombor versi protokol NTP (1-4).

mod (mod) β€” mod operasi penghantar paket. Nilai dari 0 hingga 7, paling biasa:

  • 3 - pelanggan
  • 4 - pelayan
  • 5 - mod siaran

Lapisan (tahap lapisan) - bilangan lapisan perantaraan antara pelayan dan jam rujukan (1 - pelayan mengambil data terus dari jam rujukan, 2 - pelayan mengambil data dari pelayan dengan tahap 1, dsb.).
kolam ialah integer bertanda yang mewakili selang maksimum antara mesej berturut-turut. Pelanggan NTP menentukan di sini selang di mana ia menjangkakan untuk meninjau pelayan, dan pelayan NTP menentukan selang di mana ia menjangkakan untuk ditinjau. Nilainya adalah sama dengan logaritma binari saat.
Kepersisan (ketepatan) ialah integer bertanda yang mewakili ketepatan jam sistem. Nilainya adalah sama dengan logaritma binari saat.
kelewatan akar (kependaman pelayan) ialah masa yang diperlukan untuk jam mencapai pelayan NTP, sebagai nombor titik tetap saat.
penyebaran akar (penyebaran pelayan) - Penyerakan jam pelayan NTP sebagai nombor titik tetap saat.
Ruj id (id sumber) – id tonton. Jika pelayan mempunyai stratum 1, maka ref id ialah nama jam atom (4 aksara ASCII). Jika pelayan menggunakan pelayan lain, maka ref id mengandungi alamat pelayan ini.
4 medan terakhir ialah masa - 32 bit - bahagian integer, 32 bit - bahagian pecahan.
Rujukan - jam terkini pada pelayan.
Berasal – masa apabila paket dihantar (diisi oleh pelayan – lebih lanjut mengenai perkara di bawah).
menerima – masa apabila paket diterima oleh pelayan.
Menghantar – masa apabila paket dihantar dari pelayan kepada pelanggan (diisi oleh pelanggan, lebih lanjut mengenai perkara di bawah).

Dua medan terakhir tidak akan dipertimbangkan.

Mari tulis pakej kami:

Kod pakej

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

Untuk menghantar (dan menerima) paket ke pelayan, kita mesti dapat mengubahnya menjadi pelbagai bait.
Untuk operasi ini (dan terbalik), kami akan menulis dua fungsi - pack() dan unpack():

fungsi pek

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

membongkar fungsi

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

Bagi orang yang malas, sebagai aplikasi - kod yang menjadikan pakej menjadi rentetan yang indah

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)

Menghantar pakej ke pelayan

Hantar paket dengan medan yang diisi ke pelayan versi, mod ΠΈ Menghantar. Π’ Menghantar anda mesti menentukan masa semasa pada mesin tempatan (bilangan saat sejak 1 Januari 1900), versi - mana-mana 1-4, mod - 3 (mod pelanggan).

Pelayan, setelah menerima permintaan, mengisi semua medan dalam paket NTP, menyalin ke dalam medan Berasal nilai daripada Menghantar, yang terdapat dalam permintaan. Ia adalah misteri kepada saya mengapa pelanggan tidak boleh segera mengisi nilai masanya di lapangan Berasal. Akibatnya, apabila paket itu kembali, pelanggan mempunyai 4 nilai masa - masa permintaan dihantar (Berasal), masa pelayan menerima permintaan (menerima), masa pelayan menghantar respons (Menghantar) dan masa penerimaan maklum balas oleh pelanggan - Tiba (tiada dalam bungkusan). Dengan nilai ini kita boleh menetapkan masa yang betul.

Kod penghantaran dan penerimaan pakej

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

Pemprosesan data daripada pelayan

Pemprosesan data dari pelayan adalah serupa dengan tindakan lelaki Inggeris dari masalah lama Raymond M. Smallian (1978): "Seorang tidak mempunyai jam tangan, tetapi ada jam dinding yang tepat di rumah, yang dia kadang-kadang terlupa angin. Pada suatu hari, terlupa untuk memulakan jam semula, dia pergi melawat rakannya, menghabiskan waktu petang dengannya, dan apabila dia pulang ke rumah, dia berjaya menetapkan jam dengan betul. Bagaimanakah dia berjaya melakukan ini jika masa perjalanan tidak diketahui lebih awal? Jawapannya ialah: β€œKeluar dari rumah, seseorang menggulung jam dan mengingati kedudukan tangan. Datang kepada rakan dan meninggalkan tetamu, dia mencatat masa ketibaan dan keberangkatannya. Ini membolehkan dia mengetahui berapa lama dia pergi. Pulang ke rumah dan melihat jam, seseorang menentukan tempoh ketidakhadirannya. Menolak dari masa ini masa yang dia luangkan untuk melawat, orang itu mengetahui masa yang dihabiskan di jalan raya sana dan balik. Dengan menambah separuh masa yang dihabiskan di jalan raya kepada masa meninggalkan tetamu, dia mendapat peluang untuk mengetahui masa ketibaan di rumah dan melaraskan jarum jamnya dengan sewajarnya.

Cari masa pelayan bekerja pada permintaan:

  1. Mencari masa perjalanan paket dari klien ke pelayan: ((Tiba - Asal) - (Hantar - Terima)) / 2
  2. Cari perbezaan antara masa klien dan pelayan:
    Terima - Asal - ((Tiba - Asal) - (Hantar - Terima)) / 2 =
    2 * Terima - 2 * Asal - Tiba + Asal + Hantar - Terima =
    Terima - Asal - Tiba + Hantar

Kami menambah nilai yang diterima pada masa tempatan dan menikmati kehidupan.

Output Keputusan

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)

Berguna pautan.

Sumber: www.habr.com

Tambah komen