Здрастуйте, хабраюзери. Сьогодні я хочу розповісти про те, як написати свій простенький клієнт 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)