Kuandika mteja rahisi wa NTP

Habari za habrausers. Leo nataka kuzungumza juu ya jinsi ya kuandika mteja wako rahisi wa NTP. Kimsingi, mazungumzo yatageuka kwenye muundo wa pakiti na jinsi majibu kutoka kwa seva ya NTP yanachakatwa. Nambari hiyo itaandikwa kwa python, kwa sababu, kwa maoni yangu, hakuna lugha bora kwa vitu kama hivyo. Connoisseurs watazingatia kufanana kwa nambari na nambari ya ntplib - "niliongozwa" nayo.

Kwa hivyo, NTP ni nini? NTP ni itifaki ya kuwasiliana na seva za wakati. Itifaki hii hutumiwa katika mashine nyingi za kisasa. Kwa mfano, huduma ya w32tm kwenye madirisha.

Kuna matoleo 5 ya itifaki ya NTP kwa jumla. Toleo la kwanza, 0 (1985, RFC958) kwa sasa linachukuliwa kuwa halitumiki. Mpya zaidi zinatumika kwa sasa, ya 1 (1988, RFC1059), ya 2 (1989, RFC1119), ya 3 (1992, RFC1305) na ya 4 (1996, RFC2030). Matoleo 1-4 yanaendana na kila mmoja, yanatofautiana tu katika algorithms ya seva.

Muundo wa Pakiti

Kuandika mteja rahisi wa NTP

Kiashiria cha kurukaruka (kiashiria cha kusahihisha) ni nambari inayoonyesha onyo la pili la kurukaruka. Maana:

  • 0 - hakuna marekebisho
  • 1 - dakika ya mwisho ya siku ina sekunde 61
  • 2 - dakika ya mwisho ya siku ina sekunde 59
  • 3 - kutofaulu kwa seva (muda umeisha kusawazisha)

Nambari ya toleo (nambari ya toleo) - Nambari ya toleo la itifaki ya NTP (1-4).

mode (mode) - hali ya uendeshaji ya mtumaji wa pakiti. Thamani kutoka 0 hadi 7, ya kawaida zaidi:

  • 3 - mteja
  • 4 - seva
  • 5 - hali ya utangazaji

Tabaka (ngazi ya safu) - idadi ya tabaka za kati kati ya seva na saa ya kumbukumbu (1 - seva inachukua data moja kwa moja kutoka kwa saa ya kumbukumbu, 2 - seva inachukua data kutoka kwa seva na kiwango cha 1, nk).
Pool ni nambari kamili iliyotiwa sahihi inayowakilisha muda wa juu zaidi kati ya barua pepe zinazofuatana. Kiteja cha NTP kinabainisha hapa muda ambapo kinatarajia kupigia kura seva, na seva ya NTP inabainisha muda ambao inatarajia kupigwa kura. Thamani ni sawa na logarithm binary ya sekunde.
Precision (usahihi) ni nambari kamili iliyotiwa saini inayowakilisha usahihi wa saa ya mfumo. Thamani ni sawa na logarithm binary ya sekunde.
kuchelewa kwa mizizi (muda wa kusubiri wa seva) ni muda unaochukua kwa saa kufikia seva ya NTP, kama nambari ya uhakika ya sekunde.
mtawanyiko wa mizizi (kutawanya kwa seva) - Mtawanyiko wa saa ya seva ya NTP kama nambari ya uhakika ya sekunde.
Kitambulisho cha kumbukumbu (kitambulisho cha chanzo) - kitambulisho cha kutazama. Ikiwa seva ina tabaka 1, basi kitambulisho cha ref ni jina la saa ya atomiki (herufi 4 za ASCII). Ikiwa seva hutumia seva nyingine, basi kitambulisho cha ref kina anwani ya seva hii.
Sehemu 4 za mwisho ni wakati - biti 32 - sehemu kamili, biti 32 - sehemu ya sehemu.
Reference - saa ya hivi karibuni kwenye seva.
Chimbuko - wakati ambapo pakiti ilitumwa (iliyojazwa na seva - zaidi juu ya hapo chini).
Pokea - wakati ambapo pakiti ilipokelewa na seva.
Kusambaza - wakati ambapo pakiti ilitumwa kutoka kwa seva hadi kwa mteja (iliyojazwa na mteja, zaidi juu ya hapo chini).

Sehemu mbili za mwisho hazitazingatiwa.

Wacha tuandike kifurushi chetu:

Msimbo wa kifurushi

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

Ili kutuma (na kupokea) pakiti kwa seva, ni lazima tuweze kuigeuza kuwa safu ya baiti.
Kwa operesheni hii (na nyuma), tutaandika kazi mbili - pakiti () na unpack():

pakiti kazi

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

kitendakazi cha kufungua

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

Kwa watu wavivu, kama programu - nambari inayogeuza kifurushi kuwa kamba nzuri

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)

Inatuma kifurushi kwa seva

Tuma pakiti iliyo na sehemu zilizojazwa kwa seva version, mode ΠΈ Kusambaza. Katika Kusambaza lazima ueleze wakati wa sasa kwenye mashine ya ndani (idadi ya sekunde tangu Januari 1, 1900), toleo - yoyote ya 1-4, mode - 3 (mode ya mteja).

Seva, baada ya kupokea ombi, inajaza sehemu zote kwenye pakiti ya NTP, inakili kwenye uwanja Chimbuko thamani kutoka Kusambaza, ambayo ilikuja katika ombi. Ni siri kwangu kwa nini mteja hawezi kujaza mara moja thamani ya muda wake shambani Chimbuko. Kama matokeo, wakati pakiti inarudi, mteja ana maadili 4 ya wakati - wakati ombi lilitumwa (Chimbuko), wakati seva ilipokea ombi (Pokea), wakati seva ilituma majibu (Kusambaza) na wakati wa kupokea jibu na mteja - Fika (sio kwenye kifurushi). Kwa maadili haya tunaweza kuweka wakati sahihi.

Kifurushi cha kutuma na kupokea msimbo

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

Usindikaji wa data kutoka kwa seva

Usindikaji wa data kutoka kwa seva ni sawa na vitendo vya bwana wa Kiingereza kutoka kwa shida ya zamani ya Raymond M. Smallian (1978): "Mtu mmoja hakuwa na saa ya mkono, lakini kulikuwa na saa sahihi ya ukutani nyumbani, ambayo aliifanya. wakati mwingine alisahau upepo. Siku moja, akisahau kuanza saa tena, alikwenda kumtembelea rafiki yake, akakaa naye jioni, na aliporudi nyumbani, aliweza kuweka saa kwa usahihi. Aliwezaje kufanya hivyo ikiwa wakati wa kusafiri haukujulikana mapema? Jibu ni: "Kuondoka nyumbani, mtu huinua saa na kukumbuka nafasi ya mikono. Kuja kwa rafiki na kuacha wageni, anabainisha wakati wa kuwasili na kuondoka kwake. Hii inamruhusu kujua ni muda gani alikuwa mbali. Kurudi nyumbani na kuangalia saa, mtu huamua muda wa kutokuwepo kwake. Kuondoa kutoka wakati huu wakati ambao alitumia kutembelea, mtu hupata wakati uliotumiwa barabarani huko na kurudi. Kwa kuongeza nusu ya muda uliotumika kwenye barabara hadi wakati wa kuondoka kwa wageni, anapata fursa ya kujua wakati wa kuwasili nyumbani na kurekebisha mikono ya saa yake ipasavyo.

Tafuta saa ambayo seva ilikuwa ikifanya kazi kwa ombi:

  1. Kupata muda wa kusafiri kwa pakiti kutoka kwa mteja hadi kwa seva: ((Fikia - Chanzia) - (Sambaza - Pokea)) / 2
  2. Pata tofauti kati ya mteja na wakati wa seva:
    Pokea - Chanzo - ((Fikia - Chanzia) - (Sambaza - Pokea)) / 2 =
    2 * Pokea - 2 * Chanza - Fika + Chanza + Sambaza - Pokea =
    Pokea - Chanzia - Fika + Sambaza

Tunaongeza thamani iliyopokelewa kwa wakati wa ndani na kufurahia maisha.

Pato la Matokeo

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)

Inafaa kiungo.

Chanzo: mapenzi.com

Kuongeza maoni