Պարզ NTP հաճախորդ գրելը

Ողջույն, Հաբրաուզերներ: Այսօր ես ուզում եմ խոսել այն մասին, թե ինչպես գրել ձեր սեփական պարզ NTP հաճախորդը: Հիմնականում խոսակցությունը կվերածվի փաթեթի կառուցվածքին և NTP սերվերից պատասխանի մշակման եղանակին: Կոդը գրվելու է Python-ով, քանի որ ինձ թվում է, որ նման բաների համար ավելի լավ լեզու պարզապես չկա։ Գիտակները կնշեն կոդի նմանությունը ntplib կոդի հետ. ես «ոգեշնչված» էի դրանից:

Այսպիսով, ինչ է իրականում NTP-ն: NTP-ը ճշգրիտ ժամային սերվերների հետ փոխգործակցության արձանագրություն է: Այս արձանագրությունն օգտագործվում է շատ ժամանակակից մեքենաներում: Օրինակ՝ w32tm ծառայությունը windows-ում։

Ընդհանուր առմամբ կա 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 - հեռարձակման ռեժիմ

Stratum- ը (շերտավորման մակարդակ) – սերվերի և հղման ժամացույցի միջև միջանկյալ շերտերի քանակը (1 – սերվերը տվյալներ է վերցնում անմիջապես հղումային ժամացույցից, 2 – սերվերը տվյալներ է վերցնում 1-ին շերտ ունեցող սերվերից և այլն):
Ավազան ստորագրված ամբողջ թիվ է, որը ներկայացնում է հաջորդական հաղորդագրությունների միջև առավելագույն միջակայքը: NTP հաճախորդն այստեղ նշում է այն միջակայքը, որով նա ակնկալում է հարցում կատարել սերվերի վրա, իսկ NTP սերվերը նշում է այն ինտերվալը, որով նա ակնկալում է հարցում կատարել: Արժեքը հավասար է վայրկյանների երկուական լոգարիթմին:
Դիպուկություն (ճշտությունը) ստորագրված ամբողջ թիվ է, որը ներկայացնում է համակարգի ժամացույցի ճշգրտությունը: Արժեքը հավասար է վայրկյանների երկուական լոգարիթմին:
Արմատային ուշացում (սերվերի հետաձգում) – ժամացույցի ընթերցումների NTP սերվերին հասնելու ժամանակը, որպես վայրկյանների ֆիքսված կետ:
Արմատային դիսպերսիա (սերվերի տարածում) - NTP սերվերի ժամացույցի ընթերցումների տարածումը ֆիքսված կետով վայրկյանների քանակով:
Ref 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))

unpack գործառույթը

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)

Օգտակար ՈՒղեցույց.

Source: www.habr.com

Добавить комментарий