Здравейте хабраузери. Днес искам да говоря за това как да напишете свой собствен прост NTP клиент. По принцип разговорът ще се обърне към структурата на пакета и как се обработва отговорът от NTP сървъра. Кодът ще бъде написан на python, защото според мен просто няма по-добър език за такива неща. Познавачите ще обърнат внимание на приликата на кода с кода на ntplib - бях "вдъхновен" от него.
И така, какво е NTP? NTP е протокол за комуникация със сървъри за време. Този протокол се използва в много съвременни машини. Например услугата w32tm на Windows.
Има общо 5 версии на NTP протокола. Първата, версия 0 (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 сървъра, като брой секунди с фиксирана точка.
коренова дисперсия (server scatter) - Разсейването на часовника на NTP сървъра като брой секунди с фиксирана запетая.
Реф (идентификатор на източник) – идентификационен номер за гледане. Ако сървърът има слой 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)
Изпращане на пакет до сървъра
Изпратете пакет с попълнени полета до сървъра версия, вид и Предава. В Предава трябва да посочите текущото време на локалната машина (брой секунди от 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 = "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