كتابة عميل NTP بسيط

مرحبًا يا هابروزر. اليوم أريد أن أتحدث عن كيفية كتابة عميل NTP البسيط الخاص بك. في الأساس، ستنتقل المحادثة إلى بنية الحزمة وكيفية معالجة الاستجابة من خادم NTP. سيتم كتابة الكود بلغة بايثون، لأنه، في رأيي، لا توجد لغة أفضل لمثل هذه الأشياء. سوف ينتبه الخبراء إلى تشابه الكود مع كود ntplib - لقد "ألهمتني" به.

إذن ما هو NTP على أية حال؟ NTP هو بروتوكول للتواصل مع خوادم الوقت. يُستخدم هذا البروتوكول في العديد من الأجهزة الحديثة. على سبيل المثال، خدمة w32tm في نظام التشغيل Windows.

هناك 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 كعدد ثابت من الثواني.
معرف المرجع (معرف المصدر) - معرف المشاهدة. إذا كان الخادم يحتوي على الطبقة 1، فإن معرف المرجع هو اسم الساعة الذرية (4 أحرف ASCII). إذا كان الخادم يستخدم خادمًا آخر، فإن معرف المرجع يحتوي على عنوان هذا الخادم.
الحقول الأربعة الأخيرة هي الوقت - 4 بت - الجزء الصحيح، 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

إضافة تعليق