نوشتن یک کلاینت ساده NTP

سلام هابراوسرز. امروز می خواهم در مورد نحوه نوشتن کلاینت ساده NTP خود صحبت کنم. اساساً، مکالمه به ساختار بسته و روش پردازش پاسخ از سرور NTP خواهد رفت. کد به زبان پایتون نوشته خواهد شد، زیرا به نظر من زبان بهتری برای چنین چیزهایی وجود ندارد. خبره ها به شباهت کد با کد ntplib توجه خواهند کرد - من از آن "الهام گرفتم".

بنابراین NTP دقیقا چیست؟ NTP یک پروتکل برای تعامل با سرورهای زمان دقیق است. این پروتکل در بسیاری از ماشین های مدرن استفاده می شود. برای مثال سرویس w32tm در ویندوز.

در کل 5 نسخه از پروتکل NTP وجود دارد. نسخه اول، نسخه 0 (1985، RFC958))، در حال حاضر منسوخ در نظر گرفته می شود. اکنون جدیدترها استفاده می شوند، اول (1، RFC1988)، دوم (1059، RFC2)، سوم (1989، RFC1119) و چهارم (3، RFC1992). نسخه های 1305-4 با یکدیگر سازگار هستند، آنها فقط در الگوریتم های عملکرد سرور متفاوت هستند.

قالب بسته

نوشتن یک کلاینت ساده NTP

نشانگر جهش (نشانگر تصحیح) - عددی که نشان دهنده هشدار در مورد هماهنگی دوم است. معنی:

  • 0 - بدون اصلاح
  • 1- آخرین دقیقه روز شامل 61 ثانیه است
  • 2- آخرین دقیقه روز شامل 59 ثانیه است
  • 3 - خرابی سرور (زمان هماهنگ نیست)

شماره نسخه (شماره نسخه) - شماره نسخه پروتکل NTP (1-4).

حالت (حالت) - حالت عملکرد فرستنده بسته. مقدار از 0 تا 7، رایج ترین:

  • 3- مشتری
  • 4 - سرور
  • 5 – حالت پخش

قشر (سطح لایه بندی) - تعداد لایه های میانی بین سرور و ساعت مرجع (1 - سرور داده ها را مستقیماً از ساعت مرجع می گیرد، 2 - سرور داده ها را از یک سرور با لایه 1 و غیره می گیرد).
استخر یک عدد صحیح امضا شده است که نشان دهنده حداکثر فاصله بین پیام های متوالی است. سرویس گیرنده NTP در اینجا فاصله زمانی را که انتظار دارد از سرور نظرسنجی کند و سرور NTP بازه زمانی را که انتظار دارد در آن نظرسنجی انجام شود را مشخص می کند. مقدار برابر با لگاریتم باینری ثانیه است.
دقت (دقت) یک عدد صحیح امضا شده است که نشان دهنده دقت ساعت سیستم است. مقدار برابر با لگاریتم باینری ثانیه است.
تاخیر ریشه (تاخیر سرور) - زمانی که طول می کشد تا قرائت های ساعت به سرور NTP برسند، به عنوان یک عدد ثابت ثانیه.
پراکندگی ریشه (گسترش سرور) - انتشار قرائت های ساعت سرور NTP به صورت چند ثانیه با یک نقطه ثابت.
Ref ID (شناسه منبع) - شناسه ساعت. اگر سرور دارای لایه 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)

ارسال بسته به سرور

یک بسته با فیلدهای پر شده باید به سرور ارسال شود نسخه, حالت и انتقالاست. به انتقال شما باید زمان فعلی را در دستگاه محلی (تعداد ثانیه از 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

اضافه کردن نظر