Писање једноставног НТП клијента

Здраво хабраузери. Данас желим да причам о томе како написати свој једноставан НТП клијент. У основи, разговор ће се окренути структури пакета и начину на који се обрађује одговор са НТП сервера. Код ће бити написан на питону, јер, по мом мишљењу, једноставно нема бољег језика за такве ствари. Познаваоци ће обратити пажњу на сличност кода са нтплиб кодом – мене је то „инспирисало“.

Па шта је уопште НТП? НТП је протокол за комуникацију са временским серверима. Овај протокол се користи у многим модерним машинама. На пример, услуга в32тм на Виндовс-у.

Укупно постоји 5 верзија НТП протокола. Прва, верзија 0 (1985, РФЦ958) се тренутно сматра застарелом. Тренутно се користе новији, 1. (1988, РФЦ1059), 2. (1989, РФЦ1119), 3. (1992, РФЦ1305) и 4. (1996, РФЦ2030). Верзије 1-4 су компатибилне једна са другом, разликују се само у алгоритмима сервера.

Формат пакета

Писање једноставног НТП клијента

Индикатор скока (индикатор корекције) је број који означава упозорење о преступном другом. Значење:

  • 0 - нема корекције
  • 1 - последњи минут дана садржи 61 секунду
  • 2 - последњи минут дана садржи 59 секунди
  • 3 - грешка сервера (време није синхронизовано)

Верзија број (број верзије) – број верзије НТП протокола (1-4).

начин (режим) — режим рада пошиљаоца пакета. Вредност од 0 до 7, најчешће:

  • 3 - клијент
  • 4 - сервер
  • 5 - режим емитовања

Стратум (ниво слојевитости) - број међуслојева између сервера и референтног сата (1 - сервер преузима податке директно са референтног сата, 2 - сервер преузима податке са сервера са нивоом 1, итд.).
Базен је цео број са предзнаком који представља максимални интервал између узастопних порука. НТП клијент овде специфицира интервал у којем очекује да ће анкетирати сервер, а НТП сервер специфицира интервал у којем очекује да ће бити анкетиран. Вредност је једнака бинарном логаритму секунди.
Прецизност (прецизност) је цео број са предзнаком који представља тачност системског сата. Вредност је једнака бинарном логаритму секунди.
кашњење корена (латенција сервера) је време потребно да сат стигне до НТП сервера, као број секунди са фиксном тачком.
дисперзија корена (сцаттер сервера) – Сцаттер сата НТП сервера као број секунди са фиксном тачком.
Реф ид (ИД извора) – ид гледања. Ако сервер има стратум 1, онда је реф ид име атомског сата (4 АСЦИИ знака). Ако сервер користи други сервер, онда реф ид садржи адресу овог сервера.
Последња 4 поља су време - 32 бита - цео део, 32 бита - разломак.
Препорука - најновији сат на серверу.
Оригинате – време када је пакет послат (попуњава сервер – више о томе у наставку).
Примање – време када је сервер примио пакет.
Слати – време када је пакет послат са сервера на клијента (попуњава клијент, више о томе у наставку).

Последња два поља неће бити разматрана.

Хајде да напишемо наш пакет:

Шифра пакета

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

Да бисмо послали (и примили) пакет на сервер, морамо бити у могућности да га претворимо у низ бајтова.
За ову (и обрнуто) операцију, написаћемо две функције - пацк() и унпацк():

функција паковања

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

функција распакивања

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

За лењиве, као апликација - код који претвара пакет у леп низ

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)

Слање пакета на сервер

Пошаљите пакет са попуњеним пољима на сервер верзија, начин и Слати. У Слати морате да наведете тренутно време на локалној машини (број секунди од 1. јануара 1900), верзију - било која од 1-4, режим - 3 (режим клијента).

Сервер, након што је примио захтев, попуњава сва поља у НТП пакету, копирајући у поље Оригинате вредност од Слати, који је дошао у захтеву. За мене је мистерија зашто клијент не може одмах да попуни вредност свог времена на терену Оригинате. Као резултат тога, када се пакет врати, клијент има 4 временске вредности - време када је захтев послан (Оригинате), време када је сервер примио захтев (Примање), време када је сервер послао одговор (Слати) и време пријема одговора од стране клијента - стићи (није у пакету). Са овим вредностима можемо подесити тачно време.

Шифра за слање и пријем пакета

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

Обрада података са сервера

Обрада података са сервера слична је поступцима енглеског господина из старог проблема Раимонда М. Смаллиана (1978): „Једна особа није имала ручни сат, али је код куће постојао тачан зидни сат који је он понекад заборавио да навије. Једног дана, заборавивши да поново покрене сат, отишао је у посету свом пријатељу, провео вече са њим, а када се вратио кући, успео је да тачно подеси сат. Како је то успео ако време путовања није било унапред познато? Одговор је: „Излазећи из куће, човек навија сат и памти положај казаљки. Долазећи код пријатеља и напуштајући госте, бележи време његовог доласка и одласка. Ово му омогућава да сазна колико је дуго био одсутан. Враћајући се кући и гледајући на сат, особа одређује трајање свог одсуства. Одузимајући од овог времена време које је провео у посети, особа сазнаје време проведено на путу тамо и назад. Додавањем половине времена проведеног на путу времену одласка гостију добија прилику да сазна време доласка кући и према томе подеси казаљке свог сата.

Пронађите време када је сервер радио на захтеву:

  1. Проналажење времена путовања пакета од клијента до сервера: ((Долазак - Извор) - (Пренос - Пријем)) / 2
  2. Пронађите разлику између времена клијента и сервера:
    Прими - Покрени - ((Долазак - Извор) - (Пренеси - Пријем)) / 2 =
    2 * Пријем - 2 * Оригинате - Долазак + Оригинате + Трансмит - Пријем =
    Пријем - Оригинате - Долазак + Пренос

Добијену вредност додајемо локалном времену и уживамо у животу.

Резултат Излаз

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)

Корисно веза.

Извор: ввв.хабр.цом

Додај коментар