Здравейте, хабраюзери. Днес ще ви разкажа как да напишете прост NTP клиент. Основно ще обсъдим структурата на пакета и начина на обработка на отговора от NTP сървъра. Кодът ще бъде написан на Python, защото, както ми се струва, няма по-подходящ език за подобни неща. Запознати ще забележат сходството с кода на ntplib — именно той ме вдъхнови.
И така, какво всъщност представлява NTP? NTP е протокол за взаимодействие със сървъри за прецизно време. Този протокол се използва в много съвременни устройства. Например, услугата w32tm в Windows.
Съществуват общо 5 версии на NTP протокола. Първата, нулевата версия (1985 г., RFC958), в момента се счита за остаряла. В момента се използват по-нови, 1-ва (1988, RFC1059), 2-ра(1989, RFC1119), 3-та(1992, RFC1305) и 4-та(1996, RFC2030). Версиите 1-4 са съвместими помежду си, те се различават само по алгоритмите на работа. сървъри.
Формат на пакета

Индикатор за скок (индикатор на корекция) — число, което показва предупреждение за секунда на координация. Стойност:
- 0 – без корекция
- 1 – последната минута на деня съдържа 61 секунда
- 2 – последната минута на деня съдържа 59 секунди
- 3 – неизправност на сървъра (времето не е синхронизирано)
Номер на версия (номер на версия) – номер на версията на NTP протокола (1-4).
Режим (режим) — режим на работа на изпращача на пакета. Стойност от 0 до 7, най-чести:
- 3 – клиент
- 4 – сървър
- 5 – широковещателен режим
Стратум (ниво на наслояване) – брой междинни слоеве между сървъра и референтните часовници (1 – сървърът получава данните директно от референтните часовници, 2 – сървърът получава данни от сървър с ниво 1 и т.н.).
Пул — цяло число със знак, представляващо максималния интервал между последователните съобщения. NTP клиентът посочва тук интервала, с който планира да запитва сървъра, а NTP сървърът – интервала, с който планира да бъде запитван. Стойността е равна на двоичния логаритъм на секундите.
Прецизност (точност) — цяло число със знак, представляващо точността на системните часовници. Стойността е равна на двоичния логаритъм на секундите.
Коренна забавяне (забавяне на сървъра) – времето, за което показанията на часовниците достигат до NTP сървъра, записано като число секунди с фиксирана запетая.
Коренна дисперсия (разброс на показанията на сървъра) — разбросът в показанията на NTP сървъра, записан като число секунди с фиксирана запетая.
Референтен 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):
# Необходимо е за влизане на лийп секунда (2 бита)
self.leap_indicator = 0
# Версия на протокола (3 бита)
self.version_number = version_number
# Режим на изпращача (3 бита)
self.mode = mode
# Нивото на "слоестост" четене на времето (1 байт)
self.stratum = 0
# Интервал между заявките (1 байт)
self.pool = 0
# Прецизност (log2) (1 байт)
self.precision = 0
# Интервал за достигане на NTP сървера (4 байта)
self.root_delay = 0
# Разсейване на часа на NTP-сървера (4 байта)
self.root_dispersion = 0
# Индикатор на часовниците (4 байта)
self.ref_id = 0
# Последно обновление време на сървера (8 байта)
self.reference = 0
# Време на изпращане на пакета от локалната машина (8 байта)
self.originate = 0
# Време на получаване на сървъра (8 байта)
self.receive = 0
# Време на изпращане на отговор от сървера (8 байта)
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)
Изпращане на пакета на сървъра
На сървъра трябва да се изпрати пакет с попълнени полета Version, Режим и Изпращане. В Изпращане необходимо е да се укаже текущото време на локалната машина (броя на секундите от 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): «Един човек нямаше ръчни часовници, но у дома му висеха точни стенни часовници, които понякога забравяше да навива. Веднъж, забравяйки за пореден път да навие часовниците, той отиде на гости на приятеля си, прекара при него вечер, а след като се върна у дома, успя да настрои часовниците. Как успя да направи това, ако времето в пътя предварително не беше известно?» Отговорът е: «Излизайки от дома, човекът навива часовниците и запомня в какво положение са стрелките. Като пристига при приятеля си и си тръгва от там, той отбелязва времето на пристигане и напускане. Това му позволява да узнае колко време е прекарал на гости. Връщайки се у дома и поглеждайки часовниците, човекът определя продължителността на отсъствието си. Изваждайки от това време времето, прекарано на гости, той узнава времето, прекарано за пътуването дотам и обратно. Добавяйки към времето на напускане на гостите половината от времето, изразходвано за пътуване, той получава възможността да узнае времето на пристигане у дома и да премести стрелките на часовниците си съответно.»
Намираме времето на работа на сървъра над заявката:
- Намираме времето за пътуване на пакета от клиента до сървъра: ((Пристигане – Произход) — (Предаване – Получаване)) / 2
- Намираме разликата между времето на клиента и времето на сървъра:
Получаване — Произход — ((Пристигане – Произход) — (Предаване – Получаване)) / 2 =
2 * Получаване — 2 * Произход – Пристигане + Произход + Предаване – Получаване =
Получаване – Произход – Пристигане + Предаване
Добавяме полученото значение към локалното време и се радваме на живота.
Извеждане на резултата
time_different = answer.get_time_different(arrive_time)
result = "Разлика във времето: {}nВреме на сървъра: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Полезно .
Източник: habr.com
