Энгийн NTP үйлчлүүлэгч бичих

Сайн уу, Хабраузерууд. Өнөөдөр би өөрийн энгийн NTP клиентээ хэрхэн бичих талаар ярихыг хүсч байна. Үндсэндээ яриа нь пакетийн бүтэц, NTP серверийн хариуг боловсруулах арга руу шилжих болно. Код нь Python дээр бичигдэх болно, учир нь ийм зүйлд илүү сайн хэл байхгүй юм шиг санагдаж байна. Мэргэжилтнүүд код нь ntplib кодтой ижил төстэй байгааг анзаарах болно - би үүнээс "онгод авсан".

Тэгэхээр NTP гэж яг юу вэ? NTP нь тодорхой цагийн серверүүдтэй харилцах протокол юм. Энэ протоколыг орчин үеийн олон машинуудад ашигладаг. Жишээлбэл, Windows дахь w32tm үйлчилгээ.

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 - өргөн нэвтрүүлгийн горим

Давхарга (давхаргын түвшин) – сервер ба лавлагааны цагийн хоорондох завсрын давхаргын тоо (1 – сервер лавлагааны цагнаас шууд мэдээлэл авдаг, 2 – сервер 1-р давхаргатай серверээс өгөгдөл авдаг гэх мэт).
Pool дараалсан мессежүүдийн хоорондох хамгийн их интервалыг илэрхийлсэн тэмдэгт бүхэл тоо юм. Энд 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))

задлах функц

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)

Сервер рүү багц илгээж байна

Дүүргэгдсэн талбар бүхий багцыг сервер рүү илгээх ёстой хувилбар, горим и илгээх. The илгээх та локал машин дээрх одоогийн цагийг (1 оны 1900-р сарын 1-ээс хойшхи секундын тоо), хувилбар - 4-3-ийн аль нэг, горим - XNUMX (үйлчлүүлэгчийн горим) зааж өгөх ёстой.

Сервер нь хүсэлтийг хүлээн авсны дараа 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

сэтгэгдэл нэмэх