ရိုးရှင်းသော NTP client ကိုရေးသားခြင်း။

မင်္ဂလာပါ Habrusers။ ဒီနေ့တော့ မင်းရဲ့ကိုယ်ပိုင် NTP client ကို ဘယ်လိုရေးရမလဲဆိုတာ ပြောပြချင်ပါတယ်။ အခြေခံအားဖြင့်၊ စကားဝိုင်းသည် ပက်ကတ်၏ဖွဲ့စည်းပုံနှင့် NTP ဆာဗာမှ တုံ့ပြန်မှုကို လုပ်ဆောင်သည့်နည်းလမ်းသို့ ပြောင်းလဲသွားမည်ဖြစ်သည်။ ကုဒ်ကို Python ဖြင့် ရေးသားလိမ့်မည်၊ အကြောင်းမှာ ထိုသို့သော အရာများအတွက် ပိုမိုကောင်းမွန်သော ဘာသာစကား မရှိဟု ယူဆပါသည်။ Connoisseurs များသည် ntplib ကုဒ်နှင့် ကုဒ်၏ တူညီမှုကို သတိပြုမိလိမ့်မည် - ၎င်းကြောင့် ကျွန်ုပ်သည် “စိတ်အားထက်သန်” ခဲ့သည်။

ဒါဆို NTP ဆိုတာဘာလဲ။ NTP သည် အချိန်အတိအကျဆာဗာများနှင့် အပြန်အလှန်တုံ့ပြန်ရန်အတွက် ပရိုတိုကောတစ်ခုဖြစ်သည်။ ဤပရိုတိုကောကို ခေတ်မီစက်များစွာတွင် အသုံးပြုသည်။ ဥပမာအားဖြင့်၊ windows ရှိ w32tm ဝန်ဆောင်မှု။

စုစုပေါင်း NTP ပရိုတိုကော ဗားရှင်း 5 ခုရှိသည်။ ပထမ၊ ဗားရှင်း 0 (1985၊ RFC958)) ကို လက်ရှိတွင် အသုံးမပြုတော့ဟု ယူဆပါသည်။ ယခု အသစ်များကို 1st (1988၊ RFC1059)၊ 2nd (1989၊ RFC1119)၊ 3rd (1992၊ RFC1305) နှင့် 4th (1996၊ RFC2030) တို့ကို အသုံးပြုထားပါသည်။ ဗားရှင်း 1-4 သည် တစ်ခုနှင့်တစ်ခု သဟဇာတဖြစ်ပြီး ၎င်းတို့သည် ဆာဗာလုပ်ဆောင်မှု အယ်လဂိုရီသမ်များတွင်သာ ကွဲပြားပါသည်။

Package ဖော်မတ်

ရိုးရှင်းသော NTP client ကိုရေးသားခြင်း။

ခုန်ပြလိုက် (ပြင်ဆင်မှုညွှန်ပြချက်) - ညှိနှိုင်းဒုတိယအကြောင်းသတိပေးညွှန်ပြသောနံပါတ်တစ်ခု။ အဓိပ္ပါယ်-

  • 0 - ပြုပြင်မှုမရှိပါ။
  • 1 - တစ်နေ့တာ၏နောက်ဆုံးမိနစ်တွင် 61 စက္ကန့်ပါဝင်သည်။
  • 2 - တစ်နေ့တာ၏နောက်ဆုံးမိနစ်တွင် 59 စက္ကန့်ပါဝင်သည်။
  • 3 – ဆာဗာချွတ်ယွင်းမှု (အချိန်ကို ထပ်တူပြု၍မရပါ)

ဗားရှင်းနံပါတ် (ဗားရှင်းနံပါတ်) – NTP ပရိုတိုကော ဗားရှင်းနံပါတ် (၁-၄)။

ပုံ (မုဒ်) — ထုပ်ပိုးပေးပို့သူ၏ လည်ပတ်မှုမုဒ်။ 0 မှ 7 အထိ၊ အသုံးအများဆုံးတန်ဖိုး-

  • 3 - ဖောက်သည်
  • 4 – ဆာဗာ
  • 5 – ထုတ်လွှင့်မှုမုဒ်

အလွှာ (အလွှာအဆင့်) – ဆာဗာနှင့် ရည်ညွှန်းနာရီကြားရှိ အလယ်အလတ်အလွှာအရေအတွက် (1 – ဆာဗာသည် ရည်ညွှန်းနာရီမှ ဒေတာကို တိုက်ရိုက်ယူသည်၊ 2 – ဆာဗာသည် အလွှာ 1 ပါသည့် ဆာဗာတစ်ခုမှ ဒေတာကို ယူသည်)။
ရေကူးကန် ဆက်တိုက်မက်ဆေ့ချ်များကြား အမြင့်ဆုံးကြားကာလကို ကိုယ်စားပြုသည့် ကိန်းပြည့်တစ်ခုဖြစ်သည်။ NTP client သည် ဆာဗာကို စစ်တမ်းကောက်ယူရန် မျှော်လင့်ထားသည့် ကြားကာလကို ဤနေရာတွင် သတ်မှတ်ပေးပြီး NTP ဆာဗာမှ ကောက်ယူရန် မျှော်လင့်ထားသည့် ကြားကာလကို သတ်မှတ်ပေးပါသည်။ တန်ဖိုးသည် စက္ကန့်၏ binary logarithm နှင့် ညီမျှသည်။
စေ့စပ်သေချာခြင်း (accuracy) သည် စနစ်နာရီ၏ တိကျမှုကို ကိုယ်စားပြုသည့် ကိန်းပြည့်ဖြစ်သည်။ တန်ဖိုးသည် စက္ကန့်၏ binary logarithm နှင့် ညီမျှသည်။
Root နှောင့်နှေးခြင်း။ (ဆာဗာနှောင့်နှေးမှု) – သတ်မှတ်ထားသော အမှတ်အရေအတွက်အဖြစ် စက္ကန့်အလိုက် NTP ဆာဗာသို့ရောက်ရှိရန် နာရီဖတ်ရှုခြင်းအတွက် လိုအပ်သောအချိန်။
အမြစ်ပြန့်နှံ့ခြင်း။ (ဆာဗာဖြန့်ကြက်မှု) - ပုံသေအမှတ်ဖြင့် စက္ကန့်အရေအတွက်အဖြစ် NTP ဆာဗာနာရီဖတ်ခြင်း၏ပျံ့နှံ့မှု။
Ref id (အရင်းအမြစ် identifier) ​​– နာရီ ID။ ဆာဗာတွင် stratum 1 ရှိပါက၊ ref id သည် အက်တမ်နာရီ (4 ASCII စာလုံး) ၏ အမည်ဖြစ်သည်။ အကယ်၍ ဆာဗာသည် အခြားဆာဗာကိုအသုံးပြုပါက၊ ကိုးကားချက် ID တွင် ဤဆာဗာ၏လိပ်စာပါရှိသည်။
နောက်ဆုံး အကွက် 4 ခုသည် အချိန် - 32 bits - ကိန်းပြည့်အပိုင်း၊ 32 bits - အပိုင်းကိန်းအပိုင်းကို ကိုယ်စားပြုသည်။
အညွှန်း - ဆာဗာပေါ်ရှိ နောက်ဆုံးပေါ် နာရီများကို ဖတ်ခြင်း။
အစပြု - ပက်ကက်ကို ပို့လိုက်သည့်အချိန် (ဆာဗာမှ ဖြည့်သွင်းသည် - ဤအရာအပေါ် နောက်ထပ်)။
ခံယူ - ဆာဗာမှ packet ကိုလက်ခံရရှိသည့်အချိန်။
ပို့ပေး - ဆာဗာမှ ကလိုင်းယင့်ထံသို့ ပက်ကတ်ကို ပေးပို့သည့်အချိန် (Client မှ ဖြည့်သွင်းထားသော၊ ဤအရာအပေါ် နောက်ထပ်)။

ကျွန်ုပ်တို့သည် နောက်ဆုံးနယ်ပယ်နှစ်ခုကို ထည့်သွင်းစဉ်းစားမည်မဟုတ်ပါ။

ကျွန်ုပ်တို့၏ package ကိုရေးကြပါစို့။

အထုပ်ကုဒ်

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

ဆာဗာသို့ ပက်ကတ်တစ်ခု ပေးပို့ခြင်း (လက်ခံခြင်း) ပြုလုပ်ရန်၊ ၎င်းကို byte ခင်းကျင်းတစ်ခုအဖြစ် ပြောင်းလဲနိုင်ရပါမည်။
ဤ (နှင့် ပြောင်းပြန်) လုပ်ဆောင်ချက်အတွက်၊ ကျွန်ုပ်တို့သည် လုပ်ဆောင်ချက် နှစ်ခုကို ရေးသားပါမည် - pack() နှင့် unpack():

pack လုပ်ဆောင်ချက်

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 function ကို

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 ပက်ကတ်ရှိ ကွက်လပ်အားလုံးကို ဖြည့်စွက်ကာ အကွက်ထဲသို့ ကူးယူခြင်း အစပြု ထံမှတန်ဖိုး ပို့ပေးတောင်းဆိုချက်ထဲတွင် ပါလာသည်။ ဖောက်သည်သည် လယ်ကွင်းရှိ သူ၏အချိန်တန်ဖိုးကို ချက်ချင်းမဖြည့်နိုင်ရခြင်းမှာ ကျွန်ုပ်အတွက် ဆန်းကြယ်ပါသည်။ အစပြု. ရလဒ်အနေဖြင့် packet ပြန်လာသောအခါ၊ client တွင် အချိန်တန်ဖိုး 4 ခုရှိသည် - တောင်းဆိုချက်ပေးပို့သည့်အချိန် (အစပြု) ဆာဗာသည် တောင်းဆိုချက်ကို လက်ခံရရှိသည့်အချိန် (ခံယူ) ဆာဗာမှ တုံ့ပြန်မှုပေးပို့သည့်အချိန် (ပို့ပေး) နှင့် client မှ တုံ့ပြန်မှုကို လက်ခံရရှိသည့်အချိန်- ရောက်လာ (အထုပ်ထဲတွင်မပါပါ)။ ဤတန်ဖိုးများကိုအသုံးပြုခြင်းဖြင့် ကျွန်ုပ်တို့သည် မှန်ကန်သောအချိန်ကို သတ်မှတ်နိုင်သည်။

အထုပ်ပို့ခြင်းနှင့်လက်ခံခြင်းကုဒ်

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

ဆာဗာမှ ဒေတာကို လုပ်ဆောင်နေသည်။

ဆာဗာမှ ဒေတာများကို ထုတ်ယူခြင်းသည် Raymond M. Smullyan (1978) ၏ ပြဿနာဟောင်းမှ အင်္ဂလိပ်လူကြီးမင်း၏ လုပ်ဆောင်ချက်နှင့် ဆင်တူသည်- "လူတစ်ဦးတွင် လက်ပတ်နာရီတစ်လုံးမှ မရှိသော်လည်း အိမ်တွင် တစ်ခါတစ်ရံ သူမေ့နေသော တိကျသော တိုင်ကပ်နာရီတစ်လုံးရှိသည်။ လေတိုက်ရန်။ တစ်နေ့တွင် နာရီကို နောက်တစ်ကြိမ် လေတိုက်ရန် မေ့သွားသဖြင့် သူ့သူငယ်ချင်းထံ သွားလည်ကာ ညနေခင်းတွင် အတူနေကာ အိမ်ပြန်ရောက်သောအခါ နာရီကို မှန်ကန်စွာ သတ်မှတ်နိုင်ခဲ့သည်။ ခရီးသွားချိန်ကို ကြိုမသိရင် သူဘယ်လိုလုပ်ရမလဲ။ အဖြေကတော့ “အိမ်ကထွက်သွားတဲ့အခါ လူတစ်ယောက်က နာရီကို လေတိုက်ပြီး လက်ထဲမှာရှိတဲ့ အနေအထားကို မှတ်မိတယ်။ သူငယ်ချင်းတစ်ယောက်ဆီလာပြီး ဧည့်သည်တွေကို ထားခဲ့ပြီး သူရောက်ရှိချိန်နဲ့ ထွက်ခွာချိန်ကို မှတ်သားထားခဲ့ပါတယ်။ ဤအရာက သူလာရောက်လည်ပတ်ခဲ့သည်ကို သိရှိနိုင်မည်ဖြစ်သည်။ အိမ်ပြန်ရောက်ပြီး နာရီကိုကြည့်လိုက်တော့ လူတစ်ယောက်က သူမရှိတော့တဲ့ အချိန်ကို အဆုံးအဖြတ်ပေးပါတယ်။ သူလည်ပတ်ခဲ့တဲ့အချိန်ကို နုတ်လိုက်ခြင်းအားဖြင့် အဲဒီနေရာနဲ့ အပြန်ခရီးကို ဖြတ်သန်းခဲ့ရတဲ့ အချိန်ကို လူတစ်ယောက်က သိတယ်။ ဧည့်သည်များ ထွက်ခွာချိန်အထိ လမ်းပေါ်တွင် ကုန်ဆုံးခဲ့သော အချိန်တစ်ဝက်ကို ပေါင်းထည့်ခြင်းဖြင့် အိမ်သို့ရောက်ရှိမည့်အချိန်ကို သိရှိနိုင်ပြီး နာရီလက်တံများကို ချိန်ညှိရန် အခွင့်အရေးရရှိမည်ဖြစ်သည်။”

တောင်းဆိုချက်တစ်ခုအတွက် ဆာဗာလုပ်ဆောင်နေသည့်အချိန်ကို ရှာပါ-

  1. client မှ ဆာဗာသို့ packet ၏ ခရီးသွားချိန်ကို ရှာပါ- ((ရောက်ရှိ – မူလအစ) – (ပို့ – လက်ခံ)) / ၂
  2. client နှင့် server time အကြား ခြားနားချက်ကို ရှာပါ
    လက်ခံ - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 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)

အသုံးဝင်တယ်။ link ကို.

source: www.habr.com

မှတ်ချက် Add