Sveiki, Habrā uzziņi. Šodien es vēlos pastāstīt par to, kā uzrakstīt savu vienkāršo NTP klientu. Galvenokārt runa būs par paketa struktūru un atbildes apstrādes veidu no NTP servera. Kods tiks rakstīts Python valodā, jo, manuprāt, labāka valoda šādiem uzdevumiem nav atrodama. Zinošie pamanīs kodu līdzību ar ntplib kodu — tieši tas mani iedvesmoja.
Tātad, kas vispār ir NTP? NTP ir protokols, kas nodrošina saskarsmi ar precīza laika serveriem. Šo protokolu izmanto daudzās mūsdienu ierīcēs. Piemēram, pakalpojums w32tm Windows sistēmā.
Kopumā eksistē 5 NTP protokola versijas. Pirmā, 0. versija (1985. gads, RFC958), šobrīd tiek uzskatīta par novecojušu. Tagad tiek izmantotas jaunākas versijas: 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) un 4. (1996, RFC2030). 1.-4. versijas ir savstarpēji saderīgas, tās atšķiras tikai ar darbības algoritmiem. serveriem.
Paketa formāts

Leap indicator (lēciena indikators) — skaitlis, kas norāda brīdinājumu par koordinācijas sekundi. Vērtība:
- 0 – nav korekcijas
- 1 – pēdējā dienas minūtē ir 61 sekunde
- 2 – pēdējā dienas minūtē ir 59 sekundes
- 3 – servera darbības traucējumi (laiks nav sinhronizēts)
Version number (versijas numurs) – NTP protokola versijas numurs (1-4).
Mode (režīms) — sūtītāja paketa darbības režīms. Vērtība no 0 līdz 7, visbiežāk sastopamās:
- 3 – klients
- 4 – serveris
- 5 – plašaizsūtīšanas režīms
Stratum (slāņa līmenis) – starpība starp serveri un atsauces pulksteni (1 – serveris saņem datus tieši no atsauces pulksteņa, 2 – serveris saņem datus no 1. līmeņa servera utt.).
Pool — veselais skaitlis ar zīmi, kas attēlo maksimālo intervālu starp secīgajiem ziņojumiem. NTP klients norāda šeit intervālu, ar kādu viņš plāno uzrunāt serveri, bet NTP serveris – intervālu, ar kādu viņš plāno, lai viņu uzrunātu. Vērtība ir vienāda ar bināro logaritmu sekunžu skaitā.
Precision (precizitāte) — veselais skaitlis ar zīmi, kas attēlo sistēmas pulksteņa precizitāti. Vērtība ir vienāda ar bināro logaritmu sekunžu skaitā.
Root delay (servera aizkave) – laiks, kurā pulksteņu rādījumi sasniedz NTP serveri, izteikts sekundēs ar fiksētu punktu.
Root dispersion (servera rādījumu izkliede) — NTP servera pulksteņu rādījumu izkliede kā skaitlis sekundēs ar fiksētu punktu.
Ref id (sķaidījuma identifikators) – stunda ID. Ja serverim ir stratum 1, tad ref id – atomisko stundu nosaukums (4 ASCII simboli). Ja servers izmanto citu serveri, tad ref id ir šī servera adrese.
Pēdējie 4 lauki attēlo laiku – 32 biti – vesela daļa, 32 biti – daļējā daļa.
Atsauce — pēdējie laika mērījumi serverī.
Originate – laiks, kad pakets tika nosūtīts (to aizpilda servers – par to vēlāk).
Receive – laiks, kad pakets tika saņemts serverī.
Transmit – laiks, kad pakets tika nosūtīts no servera uz klientu (to aizpilda klients, par to arī vēlāk).
Divus pēdējos laukus neapskatīsim.
Uzrakstīsim mūsu paketi:
Paketēs kods
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Nepieciešams ievadīt leaping sekundes (2 biti)
self.leap_indicator = 0
# Protokola versija (3 biti)
self.version_number = version_number
# Sūtītāja režīms (3 biti)
self.mode = mode
# "Slāņošanas" līmeņa laika lasīšana (1 byte)
self.stratum = 0
# Pieprasījumu intervāls (1 byte)
self.pool = 0
# Precizitāte (log2) (1 byte)
self.precision = 0
# Intervāls NTP servera sasniegšanai (4 bytes)
self.root_delay = 0
# Atšķirība starp NTP servera pulksteņu (4 bytes)
self.root_dispersion = 0
# Pulksteņu indikators (4 bytes)
self.ref_id = 0
# Pēdējā atjaunināšanas laiks serverī (8 bytes)
self.reference = 0
# Paketes nosūtīšanas laiks no vietējās mašīnas (8 bytes)
self.originate = 0
# Laiks, kad tika saņemts serverī (8 bytes)
self.receive = 0
# Atbildes nosūtīšanas laiks no servera (8 bytes)
self.transmit = transmit
Lai nosūtītu (un saņemtu) paketi uz serveri, mums jāspēj to pārvērst par byte masīvu.
Šai (un atgriezeniskajai) operācijai uzrakstīsim divas funkcijas — pack() un unpack():
Funkcija 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))
Funkcija 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
Tiem, kas neizdodas, kā pielikums – kods, kas pārvērš paketi skaistā virknē
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)
Paketes sūtīšana uz serveri
Ir jānosūta pakete ar aizpildītajiem laukiem uz serveri Versija, Mode un Transmit. Iz Transmit jānosaka pašreizējais laiks lokālajā mašīnā (sekundes kopš 1900. gada 1. janvāra), versija — jebkura no 1-4, režīms — 3 (klienta režīms).
Serveris, saņemot pieprasījumu, aizpilda visus laukus NTP paketē, nokopējot visus laukus Originate no saņemtas pieprasījuma vērtības. TransmitMan ir noslēpums, kāpēc klients nevar nekavējoties aizpildīt savus laika datus laukā Originate. Rezultātā, kad pakete atgriežas, klientam ir 4 laika vērtības – pieprasījuma nosūtīšanas laiks (Originate), pieprasījuma saņemšanas laiks serverī (Receive), atbildes nosūtīšanas laiks no servera (Transmit) un atbildes saņemšanas laiks no klienta – Ierašanās (nav paketē). Ar šiem datiem varam noteikt pareizu laiku.
Paketes sūtīšanas un saņemšanas kods
# 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)
Datu apstrāde no servera
Datu apstrāde no servera ir līdzīga angļu džentlmeņa darbībām no vecā Raimonda M. Smalijana uzdevuma (1978): «Vienam cilvēkam nebija rokas pulksteņa, bet mājās bija precīzi sienas pulksteņi, kurus viņš dažreiz aizmirsta uzvilkt. Reiz, aizmirsis vēlreiz uzvilkt pulksteņus, viņš devās pie sava drauga, pavadīja pie viņa vakaru un, atgriezies mājās, spēja pareizi noregulēt pulksteņus. Kā viņam tas izdevās, ja ceļojuma laiks iepriekš nebija zināms?» Atbilde ir šāda: «Pēdējo reizi izejot no mājas, cilvēks uzvelk pulksteni un piefiksē, kādā stāvoklī ir rādītāji. Aizgājis pie draugiem un iznākot no viesībām, viņš atzīmē savu ierašanās un iziešanas laiku. Tas ļauj viņam uzzināt, cik ilgi viņš atradās viesību vietā. Atgriežoties mājās un palūkājoties uz pulksteņa rādījumiem, cilvēks nosaka sava prombūtnes ilgumu. No šī laika atņemot to laiku, kuru viņš pavadīja viesībās, cilvēks noskaidro ceļojuma uz turieni un atpakaļ ilgumu. Pievienojot laika posmam, kad pameta viesus, pusi no ceļojuma laika, viņš var uzzināt, kad viņš atgriezās mājās un attiecīgi noregulēt savu pulksteni.»
Atrodam servera darba laiku uz pieprasījumu:
- Atrodam pakotnes ceļojuma laiku no klienta uz serveri: ((Arrive – Originate) — (Transmit – Receive)) / 2
- Atrodam atšķirību starp klienta un servera laiku:
Receive — Originate — ((Arrive – Originate) — (Transmit – Receive)) / 2 =
2 * Receive — 2 * Originate – Arrive + Originate + Transmit – Receive =
Receive – Originate – Arrive + Transmit
Pievienojam iegūto vērtību lokālajam laikam un priecājamies par dzīvi.
Rezultāta izvadīšana
time_different = answer.get_time_different(arrive_time)
result = "Laika atšķirība: {}nServera laiks: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Noderīgi .
Avots: habr.com
