Қарапайым NTP клиентін жазу

Сәлем, Хабраузерлер. Бүгін мен өзіңіздің қарапайым NTP клиентіңізді қалай жазу керектігін айтқым келеді. Негізінде әңгіме пакеттің құрылымына және NTP серверінен жауапты өңдеу әдісіне ауысады. Код Python тілінде жазылады, өйткені маған мұндай нәрселер үшін жақсы тіл жоқ сияқты. Білушілер кодтың ntplib кодымен ұқсастығын атап өтеді - мен оны «шабыттандырдым».

Сонымен, NTP дегеніміз не? NTP – нақты уақыт серверлерімен өзара әрекеттесу протоколы. Бұл протокол көптеген заманауи машиналарда қолданылады. Мысалы, терезелердегі w32tm қызметі.

NTP хаттамасының барлығы 5 нұсқасы бар. Бірінші, 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 серверінің сағат көрсеткіштерінің бекітілген нүктесі бар секундтар саны ретінде таралуы.
Анықтама идентификаторы (көз идентификаторы) – сағат идентификаторы. Егер серверде 1-қабат болса, онда ref id атомдық сағаттың атауы болып табылады (4 ASCII таңба). Егер сервер басқа серверді пайдаланса, онда ref идентификаторында осы сервердің мекенжайы болады.
Соңғы 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)

Серверге пакет жіберу

Толтырылған өрістері бар пакетті серверге жіберу керек нұсқа, сән и беру. The беру жергілікті машинадағы ағымдағы уақытты (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

пікір қалдыру