Viết một ứng dụng khách NTP đơn giản

Xin chào habrausers. Hôm nay tôi muốn nói về cách viết ứng dụng khách NTP đơn giản của riêng bạn. Về cơ bản, cuộc trò chuyện sẽ chuyển sang cấu trúc của gói và cách xử lý phản hồi từ máy chủ NTP. Mã này sẽ được viết bằng python, bởi vì theo tôi, đơn giản là không có ngôn ngữ nào tốt hơn cho những thứ như vậy. Những người sành sỏi sẽ chú ý đến sự giống nhau của mã này với mã ntplib - tôi đã được "truyền cảm hứng" từ nó.

Vậy NTP là gì? NTP là một giao thức để liên lạc với các máy chủ thời gian. Giao thức này được sử dụng trong nhiều máy móc hiện đại. Ví dụ như dịch vụ w32tm trên windows.

Tổng cộng có 5 phiên bản của giao thức NTP. Đầu tiên, phiên bản 0 (1985, RFC958) hiện được coi là lỗi thời. Những cái mới hơn hiện đang được sử dụng, thứ nhất (1, RFC1988), thứ 1059 (2, RFC1989), thứ 1119 (3, RFC1992) và thứ 1305 (4, RFC1996). Các phiên bản 2030-1 tương thích với nhau, chúng chỉ khác nhau về thuật toán của máy chủ.

Định dạng gói

Viết một ứng dụng khách NTP đơn giản

chỉ báo nhảy vọt (chỉ báo hiệu chỉnh) là một con số biểu thị cảnh báo giây nhảy vọt. Nghĩa:

  • 0 - không hiệu chỉnh
  • 1 - phút cuối cùng của ngày chứa 61 giây
  • 2 - phút cuối cùng của ngày chứa 59 giây
  • 3 - lỗi máy chủ (hết thời gian đồng bộ hóa)

Số phiên bản (số phiên bản) – số phiên bản giao thức NTP (1-4).

Chế độ (mode) — chế độ hoạt động của người gửi gói tin. Giá trị từ 0 đến 7, phổ biến nhất:

  • 3 - khách hàng
  • 4 - máy chủ
  • 5 - chế độ phát sóng

Địa tầng (cấp độ lớp) - số lớp trung gian giữa máy chủ và đồng hồ tham chiếu (1 - máy chủ lấy dữ liệu trực tiếp từ đồng hồ tham chiếu, 2 - máy chủ lấy dữ liệu từ máy chủ có cấp độ 1, v.v.).
Hồ Bơi là một số nguyên có dấu đại diện cho khoảng thời gian tối đa giữa các tin nhắn liên tiếp. Máy khách NTP chỉ định ở đây khoảng thời gian mà nó dự kiến ​​sẽ thăm dò ý kiến ​​của máy chủ và máy chủ NTP chỉ định khoảng thời gian mà nó dự kiến ​​sẽ được thăm dò ý kiến. Giá trị bằng logarit nhị phân của giây.
Độ chính xác (độ chính xác) là một số nguyên có dấu biểu thị độ chính xác của đồng hồ hệ thống. Giá trị bằng logarit nhị phân của giây.
độ trễ gốc (độ trễ của máy chủ) là thời gian cần thiết để đồng hồ đến máy chủ NTP, dưới dạng số giây cố định.
phát tán rễ (phân tán máy chủ) - Phân tán của đồng hồ máy chủ NTP dưới dạng số giây cố định.
ID giới thiệu (id nguồn) – id xem. Nếu máy chủ có tầng 1, thì ref id là tên của đồng hồ nguyên tử (4 ký tự ASCII). Nếu máy chủ sử dụng máy chủ khác, thì ref id chứa địa chỉ của máy chủ này.
4 trường cuối đại diện cho thời gian - 32 bit - phần nguyên, 32 bit - phần phân số.
Tài liệu tham khảo - đồng hồ mới nhất trên máy chủ.
Nguồn gốc – thời gian khi gói được gửi (do máy chủ điền – thông tin thêm về điều đó bên dưới).
Nhận – thời gian khi máy chủ nhận được gói tin.
Truyền – thời gian khi gói được gửi từ máy chủ đến máy khách (được điền bởi máy khách, thông tin chi tiết bên dưới).

Hai trường cuối cùng sẽ không được xem xét.

Hãy viết gói của chúng tôi:

mã gói

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

Để gửi (và nhận) một gói đến máy chủ, chúng ta phải có khả năng biến nó thành một mảng byte.
Đối với thao tác này (và ngược lại), chúng ta sẽ viết hai hàm - pack() và unpack():

chức năng gói

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

chức năng giải nén

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

Đối với những người lười biếng, dưới dạng một ứng dụng - mã biến gói thành một chuỗi đẹp

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)

Gửi một gói đến máy chủ

Gửi một gói với các trường đã điền tới máy chủ phiên bản, Chế độ и Truyền. Trong Truyền bạn phải chỉ định thời gian hiện tại trên máy cục bộ (số giây kể từ ngày 1 tháng 1900 năm 1), phiên bản - bất kỳ trong số 4-3, chế độ - XNUMX (chế độ máy khách).

Máy chủ, sau khi nhận được yêu cầu, điền vào tất cả các trường trong gói NTP, sao chép vào trường Nguồn gốc giá trị từ Truyền, được đưa ra trong yêu cầu. Đối với tôi, đó là một bí ẩn tại sao khách hàng không thể ngay lập tức điền vào giá trị thời gian của anh ta trong lĩnh vực này Nguồn gốc. Kết quả là khi gói tin quay lại, client có 4 giá trị thời gian - thời gian gửi yêu cầu (Nguồn gốc), thời gian máy chủ nhận được yêu cầu (Nhận), thời gian máy chủ gửi phản hồi (Truyền) và thời gian nhận được phản hồi của khách hàng - Đến (không có trong gói). Với những giá trị này, chúng ta có thể đặt thời gian chính xác.

Gói gửi và mã nhận

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

Xử lý dữ liệu từ máy chủ

Việc xử lý dữ liệu từ máy chủ tương tự như hành động của quý ông người Anh trong bài toán cũ của Raymond M. Smallian (1978): “Một người không có đồng hồ đeo tay, nhưng có một chiếc đồng hồ treo tường chính xác ở nhà, anh ta đôi khi quên gió. Một hôm, quên không khởi động lại đồng hồ, anh ta đến thăm người bạn, dành cả buổi tối với anh ta, và khi anh ta trở về nhà, anh ta đã đặt đồng hồ đúng. Làm thế nào anh ấy xoay sở để làm điều này nếu thời gian di chuyển không được biết trước? Câu trả lời là: “Ra khỏi nhà, một người lên dây cót đồng hồ và ghi nhớ vị trí của các kim. Đến với một người bạn và tiễn khách, anh ta ghi lại thời gian đến và đi. Điều này cho phép anh ta tìm ra anh ta đã đi bao lâu. Trở về nhà và nhìn đồng hồ, một người xác định thời gian vắng mặt của mình. Trừ đi thời gian mà anh ta đã đến thăm, người đó tìm ra thời gian trên đường đến đó và quay lại. Bằng cách cộng một nửa thời gian trên đường với thời gian khách rời đi, anh ta có cơ hội tìm hiểu thời gian về đến nhà và điều chỉnh kim đồng hồ cho phù hợp.

Tìm thời gian máy chủ làm việc theo yêu cầu:

  1. Tìm thời gian di chuyển gói từ máy khách đến máy chủ: ((Đến - Khởi tạo) - (Truyền - Nhận)) / 2
  2. Tìm sự khác biệt giữa thời gian của máy khách và máy chủ:
    Nhận - Xuất - ((Đến - Xuất) - (Truyền - Nhận))/2 =
    2 * Nhận - 2 * Gửi - Đến + Gửi + Truyền - Nhận =
    Nhận - Khởi tạo - Đến + Truyền

Chúng tôi thêm giá trị nhận được vào giờ địa phương và tận hưởng cuộc sống.

Đầu ra kết quả

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)

Hữu ích liên kết.

Nguồn: www.habr.com

Thêm một lời nhận xét