Пишување едноставен NTP клиент

Здраво хабраузери. Денес сакам да зборувам за тоа како да напишете свој едноставен NTP клиент. Во основа, разговорот ќе се сврти кон структурата на пакетот и начинот на обработка на одговорот од серверот NTP. Кодот ќе биде напишан во Python, затоа што ми се чини дека едноставно нема подобар јазик за такви работи. Познавачите ќе ја забележат сличноста на кодот со кодот ntplib - јас бев „инспириран“ од него.

Значи она што е NTP во секој случај? NTP е протокол за комуникација со временски сервери. Овој протокол се користи во многу современи машини. На пример, услугата w32tm во Windows.

Вкупно има 5 верзии на протоколот NTP. Првата, верзија 0 (1985, RFC958) во моментов се смета за застарена. Сега се користат поновите, 1-ви (1988, RFC1059), 2-ри (1989, RFC1119), 3-ти (1992, RFC1305) и 4-ти (1996, RFC2030). Верзиите 1-4 се компатибилни една со друга; тие се разликуваат само во алгоритмите за работа на серверот.

Пакет формат

Пишување едноставен NTP клиент

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

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

Број на верзијата (број на верзија) – број на верзија на протоколот NTP (1-4).

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

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

Стратум (ниво на слој) - бројот на средни слоеви помеѓу серверот и референтниот часовник (1 - серверот зема податоци директно од референтниот часовник, 2 - серверот зема податоци од серверот со ниво 1, итн.).
Базен е потпишан цел број што го претставува максималниот интервал помеѓу последователните пораки. Клиентот NTP овде го одредува интервалот во кој очекува да го анкетира серверот, а NTP серверот го одредува интервалот во кој очекува да биде анкетиран. Вредноста е еднаква на бинарниот логаритам од секунди.
Прецизност (точност) е потпишан цел број што ја претставува точноста на системскиот часовник. Вредноста е еднаква на бинарниот логаритам од секунди.
Одложување на коренот (латентност на серверот) е времето потребно за часовникот да стигне до серверот NTP, како број на секунди со фиксна точка.
Дисперзија на коренот (серверски расејување) - Расејувањето на часовникот на серверот NTP како број на секунди со фиксна точка.
Реф id (извор id) – часовник id. Ако серверот има слој 1, тогаш ref id е името на атомскиот часовник (4 ASCII знаци). Ако серверот користи друг сервер, тогаш ref id ја содржи адресата на овој сервер.
Последните 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

За да испратиме (и примиме) пакет на серверот, мора да можеме да го претвориме во низа од бајти.
За оваа (и обратна) операција, ќе напишеме две функции - pack() и unpack():

функција на пакет

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 (режим на клиент).

Серверот, откако го прифати барањето, ги пополнува сите полиња во пакетот NTP, копирање во полето Потекнуваат вредност од Пренесува, кој дојде во барањето. За мене е мистерија зошто клиентот не може веднаш да ја пополни вредноста на своето време на терен Потекнуваат. Како резултат на тоа, кога пакетот се враќа, клиентот има 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)

Корисно линк.

Извор: www.habr.com

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