Pagsusulat ng isang simpleng NTP client

Hello, Habrausers. Ngayon gusto kong pag-usapan kung paano magsulat ng iyong sariling simpleng NTP client. Karaniwan, ang pag-uusap ay lilipat sa istraktura ng packet at ang paraan ng pagproseso ng tugon mula sa NTP server. Ang code ay isusulat sa Python, dahil tila sa akin ay walang mas mahusay na wika para sa mga bagay na iyon. Mapapansin ng mga connoisseurs ang pagkakapareho ng code sa ntplib code - "na-inspirasyon" ako nito.

Kaya ano nga ba ang NTP? Ang NTP ay isang protocol para sa pakikipag-ugnayan sa mga server ng eksaktong oras. Ang protocol na ito ay ginagamit sa maraming modernong makina. Halimbawa, ang serbisyo ng w32tm sa mga bintana.

Mayroong kabuuang 5 bersyon ng NTP protocol. Ang una, ang bersyon 0 (1985, RFC958), ay kasalukuyang itinuturing na lipas na. May mga mas bagong bersyon na kasalukuyang ginagamit: bersyon 1 (1988, RFC1059), bersyon 2 (1989, RFC1119), bersyon 3 (1992, RFC1305), at bersyon 4 (1996, RFC2030). Ang mga bersyon 1-4 ay magkatugma sa isa't isa; ang pagkakaiba lamang nila ay sa kanilang mga operating algorithm. mga server.

Format ng package

Pagsusulat ng isang simpleng NTP client

Tagapahiwatig ng paglukso (tagapagpahiwatig ng pagwawasto) - isang numero na nagpapahiwatig ng babala tungkol sa pangalawang koordinasyon. Ibig sabihin:

  • 0 - walang pagwawasto
  • 1 – ang huling minuto ng araw ay naglalaman ng 61 segundo
  • 2 – ang huling minuto ng araw ay naglalaman ng 59 segundo
  • 3 - malfunction ng server (hindi naka-synchronize ang oras)

Numero ng bersyon (numero ng bersyon) – Numero ng bersyon ng protocol ng NTP (1-4).

paraan (mode) — operating mode ng packet sender. Halaga mula 0 hanggang 7, pinakakaraniwan:

  • 3 – kliyente
  • 4 – server
  • 5 – broadcast mode

Sapin (layering level) – ang bilang ng mga intermediate na layer sa pagitan ng server at ng reference na orasan (1 – ang server ay direktang kumukuha ng data mula sa reference na orasan, 2 – ang server ay kumukuha ng data mula sa isang server na may layer 1, atbp.).
Pul ay isang signed integer na kumakatawan sa maximum na pagitan sa pagitan ng magkakasunod na mensahe. Tinukoy dito ng kliyente ng NTP ang agwat kung saan inaasahan nitong i-poll ang server, at tinukoy ng NTP server ang agwat kung saan inaasahan nitong ma-poll. Ang halaga ay katumbas ng binary logarithm ng mga segundo.
Katumpakan (katumpakan) ay isang sign integer na kumakatawan sa katumpakan ng system clock. Ang halaga ay katumbas ng binary logarithm ng mga segundo.
Pagkaantala ng ugat (pagkaantala ng server) – ang oras na kinakailangan para sa mga pagbabasa ng orasan upang maabot ang NTP server, bilang isang fixed-point na bilang ng mga segundo.
Ang pagpapakalat ng ugat (server spread) - pagkalat ng mga pagbabasa ng orasan ng server ng NTP bilang isang bilang ng mga segundo na may nakapirming punto.
Ref id (source identifier) ​​– clock id. Kung ang server ay may stratum 1, ang ref id ay ang pangalan ng atomic clock (4 na ASCII na character). Kung gumagamit ang server ng ibang server, ang ref id ay naglalaman ng address ng server na ito.
Ang huling 4 na field ay kumakatawan sa oras - 32 bits - ang integer na bahagi, 32 bits - ang fractional na bahagi.
Sanggunian — ang pinakabagong mga pagbabasa ng orasan sa server.
Nagmula – oras kung kailan ipinadala ang packet (pinunan ng server - higit pa dito sa ibaba).
Tumanggap – oras na natanggap ng server ang packet.
Ihatid – oras ng pagpapadala ng packet mula sa server patungo sa kliyente (pinunan ng kliyente, higit pa dito sa ibaba).

Hindi namin isasaalang-alang ang huling dalawang field.

Isulat natin ang aming pakete:

Code ng package

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

Upang magpadala (at makatanggap) ng isang packet sa server, dapat natin itong gawing byte array.
Para sa operasyong ito (at baligtarin), magsusulat kami ng dalawang function - pack() at unpack():

pack function

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

i-unpack ang function

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

Para sa mga tamad na tao, bilang isang application - code na nagiging isang pakete sa isang magandang 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)

Nagpapadala ng package sa server

Ang isang packet na may mga punong field ay dapat ipadala sa server bersyon, paraan и Ihatid. Sa Ihatid dapat mong tukuyin ang kasalukuyang oras sa lokal na makina (ang bilang ng mga segundo mula noong Enero 1, 1900), bersyon - alinman sa 1-4, mode - 3 (client mode).

Ang server, na tinanggap ang kahilingan, ay pinupunan ang lahat ng mga patlang sa NTP packet, pagkopya sa field Nagmula halaga mula sa Ihatid, na dumating sa kahilingan. Ito ay isang misteryo sa akin kung bakit hindi agad mapunan ng kliyente ang halaga ng kanyang oras sa larangan Nagmula. Bilang resulta, kapag bumalik ang packet, ang kliyente ay may 4 na halaga ng oras - ang oras na ipinadala ang kahilingan (Nagmula), oras na natanggap ng server ang kahilingan (Tumanggap), oras na ipinadala ng server ang tugon (Ihatid) at ang oras na natanggap ng kliyente ang tugon – dumating (wala sa package). Gamit ang mga halagang ito maaari naming itakda ang tamang oras.

Pagpapadala at pagtanggap ng code ng package

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

Pagproseso ng data mula sa server

Ang pagproseso ng data mula sa server ay katulad ng mga aksyon ng English gentleman mula sa lumang problema ni Raymond M. Smullyan (1978): "Ang isang tao ay walang wristwatch, ngunit mayroong isang tumpak na orasan sa dingding sa bahay, na kung minsan ay nakalimutan niya. sa hangin. Isang araw, nang nakalimutan niyang i-wind muli ang kanyang relo, binisita niya ang kanyang kaibigan, nagpalipas ng gabi kasama niya, at nang umuwi siya, nagawa niyang itakda nang tama ang relo. Paano niya nagawa ito kung ang oras ng paglalakbay ay hindi alam nang maaga? Ang sagot ay: “Kapag umalis ng bahay, iniikot ng isang tao ang kanyang relo at naaalala kung anong posisyon ang mga kamay. Nang dumating sa isang kaibigan at iniwan ang mga panauhin, itinala niya ang oras ng kanyang pagdating at pag-alis. Ito ay nagpapahintulot sa kanya na malaman kung gaano katagal siya bumisita. Pagbalik sa bahay at pagtingin sa orasan, tinutukoy ng isang tao ang tagal ng kanyang pagkawala. Sa pamamagitan ng pagbabawas mula sa oras na ito ng oras na ginugol niya sa pagbisita, nalaman ng isang tao ang oras na ginugol sa paglalakbay doon at pabalik. Sa pamamagitan ng pagdaragdag ng kalahati ng oras na ginugol sa kalsada sa oras ng pag-alis ng mga bisita, nagkakaroon siya ng pagkakataong malaman ang oras ng pagdating sa bahay at ayusin ang mga kamay ng kanyang relo nang naaayon."

Hanapin ang oras na gumagana ang server sa isang kahilingan:

  1. Hanapin ang oras ng paglalakbay ng packet mula sa kliyente patungo sa server: ((Dumating – Pinagmulan) – (Ipadala – Tumanggap)) / 2
  2. Hanapin ang pagkakaiba sa pagitan ng oras ng kliyente at server:
    Tanggapin - Pinagmulan - ((Dumating - Pinagmulan) - (Ipadala - Tanggapin)) / 2 =
    2 * Tumanggap – 2 * Pinagmulan – Dumating + Pinagmulan + Ipadala – Tumanggap =
    Tanggapin – Pinagmulan – Dumating + Ipadala

Idinaragdag namin ang nagresultang halaga sa lokal na oras at nasisiyahan sa buhay.

Output ng resulta

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)

Kapaki-pakinabang link.

Pinagmulan: www.habr.com

Magdagdag ng komento