การเขียนไคลเอนต์ NTP อย่างง่าย

สวัสดีชาว Habrausers วันนี้ฉันอยากจะพูดเกี่ยวกับวิธีเขียนไคลเอนต์ NTP แบบง่าย ๆ ของคุณเอง โดยพื้นฐานแล้ว การสนทนาจะเปลี่ยนไปตามโครงสร้างของแพ็กเก็ตและวิธีการประมวลผลการตอบสนองจากเซิร์ฟเวอร์ NTP โค้ดจะเขียนด้วยภาษาไพธอน เพราะในความคิดของฉัน ไม่มีภาษาใดที่จะดีไปกว่านี้สำหรับเรื่องแบบนี้ ผู้ที่ชื่นชอบจะให้ความสนใจกับความคล้ายคลึงกันของโค้ดกับโค้ด ntplib - ฉันได้รับ "แรงบันดาลใจ" จากมัน

แล้ว NTP คืออะไรล่ะ? NTP เป็นโปรโตคอลสำหรับการสื่อสารกับเซิร์ฟเวอร์เวลา โปรโตคอลนี้ใช้ในเครื่องสมัยใหม่หลายเครื่อง ตัวอย่างเช่น บริการ w32tm บน windows

โปรโตคอล NTP มีทั้งหมด 5 เวอร์ชัน เวอร์ชันแรก 0 (1985, RFC958) ปัจจุบันถือว่าล้าสมัย รุ่นใหม่กว่ากำลังใช้อยู่ในปัจจุบัน 1st (1988, RFC1059), 2nd (1989, RFC1119), 3rd (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 จะเป็นชื่อของนาฬิกาอะตอมมิก (อักขระ ASCII 4 ตัว) หากเซิร์ฟเวอร์ใช้เซิร์ฟเวอร์อื่น รหัสอ้างอิงจะมีที่อยู่ของเซิร์ฟเวอร์นี้
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)

การประมวลผลข้อมูลจากเซิร์ฟเวอร์

การประมวลผลข้อมูลจากเซิร์ฟเวอร์นั้นคล้ายคลึงกับการกระทำของสุภาพบุรุษชาวอังกฤษจากปัญหาเก่าของ Raymond M. Smallian (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)

มีประโยชน์ ลิงค์.

ที่มา: will.com

เพิ่มความคิดเห็น