Писане на прост 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 сървъра, като брой секунди с фиксирана точка.
коренова дисперсия (server scatter) - Разсейването на часовника на NTP сървъра като брой секунди с фиксирана запетая.
Реф (идентификатор на източник) – идентификационен номер за гледане. Ако сървърът има слой 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

Добавяне на нов коментар