Добры дзень, хабраюзеры. Сёння я хачу расказаць аб тым, як напісаць свой прасценькі NTP кліент. Галоўным чынам, гутарка зойдзе аб структуры пакета і спосабе апрацоўкі адказу з NTP сервера. Код будзе напісаны на пітоне, таму што, як мне падаецца, лепшай мовы для падобных рэчаў проста не знайсці. Знаўцы звернуць увагу на падабенства кода з кодам ntplib – я "натхняўся" менавіта ім.
Такім чынам, што ўвогуле такое NTP? NTP - пратакол узаемадзеяння з серверамі дакладнага часу. Гэты пратакол выкарыстоўваецца ў шматлікіх сучасных машынах. Напрыклад, служба w32tm у windows.
Усяго існуе 5 версій NTP пратаколу. Першая, 0-я версія (1985 г, RFC958)), у дадзены момант лічыцца састарэлай. Цяпер выкарыстоўваюцца навейшыя, 1-я (1988, RFC1059), 2-я(1989, RFC1119), 3-я(1992, RFC1305) і 4-я(1996, RFC2030). 1-4 версіі з'яўляюцца сумяшчальнымі сябар з сябрам, яны адрозніваюцца толькі алгарытмамі працы сервераў.
Фармат пакета
Leap indicator (індыкатар карэкцыі) - лік, якое паказвае папярэджанне аб секундзе каардынацыі. Значэнне:
- 0 - няма карэкцыі
- 1 - апошняя хвіліна дня змяшчае 61 секунду
- 2 - апошняя хвіліна дня змяшчае 59 секунд
- 3 – няспраўнасць сервера (час не сінхранізаваны)
нумар версіі (нумар версіі) - нумар версіі пратакола NTP (1-4).
рэжым (рэжым) - рэжым працы адпраўніка пакета. Значэнне ад 0 да 7, найбольш частыя:
- 3 - кліент
- 4 - сервер
- 5 – шырокавяшчальны рэжым
Пласт (узровень напластавання) - колькасць прамежкавых пластоў паміж серверам і эталоннымі гадзінамі (1 - сервер бярэ дадзеныя непасрэдна з эталонных гадзін, 2 - сервер бярэ дадзеныя з сервера з узроўнем 1 і г.д.).
Басейн - цэлы лік са знакам, якое прадстаўляе максімальны інтэрвал паміж паслядоўнымі паведамленнямі. NTP-кліент паказвае тут інтэрвал, з якім ён мяркуе апытваць сервер, а NTP-сервер - інтэрвал, з якім ён мяркуе, каб яго апытвалі. Значэнне роўна двайковаму лагарыфму секунд.
Дакладнасць (дакладнасць) - цэлы лік са знакам, якое прадстаўляе дакладнасць сістэмных гадзін. Значэнне роўна двайковаму лагарыфму секунд.
Root delay (затрымка сервера) - час, за якое паказанні гадзін даходзяць да NTP-сервера, як лік секунд з фіксаванай кропкай.
Root dispersion (роскід паказанняў сервера) - роскід паказанняў гадзін 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():
Функцыя 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
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): «У аднаго чалавека не было наручных гадзін, але затое хаты віселі дакладныя насценныя гадзіны, якія ён часам забываў заводзіць. Аднойчы, забыўшыся чарговы раз завесці гадзіннік, ён адправіўся ў госці да свайго сябра, правёў у таго вечар, а вярнуўшыся дадому, здолеў правільна паставіць гадзіннік. Якім чынам яму ўдалося гэта зрабіць, калі час у дарозе загадзя вядома не было?» Адказ такі: «Выходзячы з дому, чалавек заводзіць гадзіннік і запамінае, у якім становішчы знаходзяцца стрэлкі. Прыйшоўшы да сябра і сыходзячы з гасцей, ён адзначае час сваёй парафіі і догляду. Гэта дазваляе яму даведацца, колькі ён знаходзіўся ў гасцях. Вярнуўшыся дадому і зірнуўшы на гадзіннік, чалавек вызначае працягласць сваёй адсутнасці. Адымаючы з таго часу той час, які ён правёў у гасцях, чалавек пазнае час, затрачаны на дарогу туды і назад. Дадаўшы да часу выхаду з гасцей палову часу, затрачанага на дарогу, ён атрымлівае магчымасць даведацца пра час прыходу дадому і перавесці адпаведным чынам стрэлкі сваіх гадзіннікаў.»
Знаходзім час працы сервера над запытам:
- Знаходзім час шляху пакета ад кліента да сервера: ((Arrive - Originate) - (Transmit - Receive)) / 2
- Знаходзім розніцу паміж часам кліента і сервера:
Receive - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 2 =
2 * Receive - 2 * Originate - Arrive + Originate + Transmit - Receive =
Receive - Originate - Arrive + Transmit
Дадаем атрыманае значэнне да лакальнага часу і цешымся жыцці.
Вывад выніку
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)