Menulis klien NTP sederhana

Halo habrausers. Hari ini saya ingin berbicara tentang cara menulis klien NTP sederhana Anda sendiri. Pada dasarnya, percakapan akan beralih ke struktur paket dan bagaimana respons dari server NTP diproses. Kode akan ditulis dengan python, karena, menurut saya, tidak ada bahasa yang lebih baik untuk hal-hal seperti itu. Penikmat akan memperhatikan kesamaan kode dengan kode ntplib - saya "terinspirasi" olehnya.

Jadi apa itu NTP? NTP adalah protokol untuk berkomunikasi dengan server waktu. Protokol ini digunakan di banyak mesin modern. Misalnya, layanan w32tm di windows.

Ada total 5 versi protokol NTP. Yang pertama, versi 0 (1985, RFC958) saat ini dianggap usang. Yang lebih baru saat ini digunakan, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) dan 4 (1996, RFC2030). Versi 1-4 kompatibel satu sama lain, mereka hanya berbeda dalam algoritme server.

Format Paket

Menulis klien NTP sederhana

Indikator lompatan (indikator koreksi) adalah angka yang menunjukkan peringatan detik kabisat. Arti:

  • 0 - tidak ada koreksi
  • 1 - menit terakhir hari itu berisi 61 detik
  • 2 - menit terakhir hari itu berisi 59 detik
  • 3 - kegagalan server (waktu tidak sinkron)

Nomor versi (nomor versi) – nomor versi protokol NTP (1-4).

mode (mode) β€” mode operasi pengirim paket. Nilai dari 0 hingga 7, paling umum:

  • 3 - klien
  • 4 - pelayan
  • 5 - mode siaran

Lapisan (tingkat pelapisan) - jumlah lapisan perantara antara server dan jam referensi (1 - server mengambil data langsung dari jam referensi, 2 - server mengambil data dari server dengan level 1, dll.).
Kolam adalah bilangan bulat bertanda yang mewakili interval maksimum antara pesan yang berurutan. Klien NTP menentukan di sini interval yang diharapkan untuk melakukan polling ke server, dan server NTP menentukan interval yang diharapkan untuk dilakukan polling. Nilainya sama dengan logaritma biner detik.
Ketelitian (presisi) adalah bilangan bulat bertanda yang mewakili keakuratan jam sistem. Nilainya sama dengan logaritma biner detik.
penundaan akar (latensi server) adalah waktu yang diperlukan jam untuk mencapai server NTP, sebagai jumlah detik titik tetap.
dispersi akar (pencar server) - Penyebaran jam server NTP sebagai jumlah detik titik tetap.
Referensi id (id sumber) – tonton id. Jika server memiliki strata 1, maka ref id adalah nama jam atom (4 karakter ASCII). Jika server menggunakan server lain, maka ref id berisi alamat server ini.
4 bidang terakhir adalah waktu - 32 bit - bagian bilangan bulat, 32 bit - bagian pecahan.
Referensi - jam terbaru di server.
Berasal – waktu ketika paket dikirim (diisi oleh server – lebih lanjut di bawah).
Menerima – waktu ketika paket diterima oleh server.
Mengirimkan – waktu ketika paket dikirim dari server ke klien (diisi oleh klien, lebih lanjut di bawah).

Dua bidang terakhir tidak akan dipertimbangkan.

Ayo tulis paket kita:

Kode paket

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 mengirim (dan menerima) paket ke server, kita harus dapat mengubahnya menjadi array byte.
Untuk operasi ini (dan sebaliknya), kami akan menulis dua fungsi - pack() dan unpack():

fungsi paket

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

fungsi membongkar

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

Untuk orang malas, sebagai aplikasi - kode yang mengubah paket menjadi string 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)

Mengirim paket ke server

Kirim paket dengan bidang yang diisi ke server Versi, mode ΠΈ Mengirimkan. Di Mengirimkan Anda harus menentukan waktu saat ini di mesin lokal (jumlah detik sejak 1 Januari 1900), versi - salah satu dari 1-4, mode - 3 (mode klien).

Server, setelah menerima permintaan, mengisi semua kolom di paket NTP, menyalinnya ke kolom tersebut Berasal nilai dari Mengirimkan, yang datang dalam permintaan. Merupakan misteri bagi saya mengapa klien tidak dapat langsung mengisi nilai waktunya di lapangan Berasal. Akibatnya, ketika paket kembali, klien memiliki 4 nilai waktu - waktu pengiriman permintaan (Berasal), saat server menerima permintaan (Menerima), waktu server mengirim respons (Mengirimkan) dan waktu penerimaan tanggapan oleh klien - Tiba (tidak ada dalam paket). Dengan nilai tersebut kita dapat mengatur waktu yang tepat.

Kode pengiriman dan penerimaan paket

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

Pemrosesan data dari server

Pemrosesan data dari server mirip dengan tindakan pria Inggris dari masalah lama Raymond M. Smallian (1978): β€œSatu orang tidak memiliki jam tangan, tetapi ada jam dinding yang akurat di rumah, yang dia kadang lupa masuk angin. Suatu hari, karena lupa menyalakan jam lagi, dia pergi mengunjungi temannya, menghabiskan malam bersamanya, dan ketika dia kembali ke rumah, dia berhasil menyetel jam dengan benar. Bagaimana dia bisa melakukan ini jika waktu tempuh tidak diketahui sebelumnya? Jawabannya adalah: β€œMeninggalkan rumah, seseorang memutar jam dan mengingat posisi jarum jam. Datang ke seorang teman dan meninggalkan para tamu, dia mencatat waktu kedatangan dan keberangkatannya. Ini memungkinkan dia untuk mengetahui berapa lama dia pergi. Pulang ke rumah dan melihat jam, seseorang menentukan durasi ketidakhadirannya. Dikurangi dari waktu ini waktu yang dia habiskan untuk berkunjung, orang tersebut mengetahui waktu yang dihabiskan di jalan bolak-balik. Dengan menambahkan separuh waktu yang dihabiskan di jalan ke waktu meninggalkan tamu, dia mendapat kesempatan untuk mengetahui waktu kedatangan di rumah dan menyesuaikan jarum jamnya.

Temukan waktu server bekerja berdasarkan permintaan:

  1. Temukan waktu perjalanan paket dari klien ke server: ((Tiba - Asal) - (Kirim - Terima)) / 2
  2. Temukan perbedaan antara waktu klien dan server:
    Terima - Asal - ((Tiba - Asal) - (Kirim - Terima)) / 2 =
    2 * Terima - 2 * Asal - Tiba + Asal + Kirim - Terima =
    Terima - Asal - Tiba + Kirim

Kami menambahkan nilai yang diterima ke waktu setempat dan menikmati hidup.

Keluaran Hasil

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 link.

Sumber: www.habr.com

Tambah komentar