6. NTP klient

Jednou ze síťových služeb, se kterými lze komunikovat na velmi nízké úrovni (byty) a přesto získat užitečnou informaci, jsou NTP servery. NTP slouží pro synchronizaci hodin počítačů a využívá UDP komunikační protokol na portu 123. Detaily o NTP protokolu a implementaci klienta v C lze najít zde.

Kompletní kód je ke stažení v gitlab repozitáři.

Vytvoření UDP klienta

Vytvoření UDP klienta je jednoduché a spočívá ve vytvoření instance třídy QUdpSocket a připojení k hostiteli. Pro práci s adresou hostitele využijeme třídu QHostInfo a připojíme se metodou QAbstractSocket::connectToHost() k některému z časových serverů, které provozuje sdružení CESNET:

  • tik.cesnet.cz, 195.113.144.201, řízen atomovými hodinami
  • tak.cesnet.cz, 195.113.144.238, řízen GPS

QHostInfo host = QHostInfo::fromName("tik.cesnet.cz");
QUdpSocket *s = new QUdpSocket;
s->connectToHost(QHostAddress(host.addresses().at(0)), 123);

Odeslání a přijetí datagramu

NTP server očekává požadavek ve formě zprávy o délce právě 48 bytů a právě tak dlouho zprávou klientovi i odpoví. Pro požadavek je klíčový první byte, jehož hodnota je 0x0B1) Existuje pochopitelně několik cest, jak takový paket vytvořit, zde využijeme toho, že na hodnotě ostatních bytů nezáleží a nastavíme všechny na stejnou hodnotu:

s->write(QByteArray(48, 0x0B));

Po přijmutí požadavku server odpoví packetem stejné velikosti. Aby klient neukončil svoji činnost dříve, než packet přijde, může využít např. blokující funkci QAbstractSocket::waitForReadyRead(), která čeká na přijetí dat. Argumentem metody je maximální doba čekání (timeout), který je defaultně nastaven na 30000 ms. Po přijetí datagramu lze datagram číst po částech metodou QIODevice::read() nebo celou najednou metodou QIODevice::readAll(). Zda jsou data ke čtení lze zjistit metodou QUdpSocket::hasPendingDatagrams().

if (s->waitForReadyRead()){
    while (s->hasPendingDatagrams()) {
        QByteArray data = s->readAll();
    }
}

Dekódování datagramu

V poli 48 bytů, které vrací server klientovi, je řada užitečných informací. Pro naše účely je zajímavá předposlední čtveřice bytů, číslo typu unsigned int, které reprezentuje počet vteřin od 1.1.1900 00:00:00. Při dekódování zprávy je třeba vyřešit dva problémy:

  • data ze serveru jsou ve formátu big endian (network byte order), ale formát dat klienta bude velmi pravděpodobně little endian. Pokud jsme si jisti endianitou klienta, lze sestavit informaci o čase ve správném pořadí. Případně lze využít funkce ntohl(), která je v OS Windows součástí knihovny WinSock
  • NTP server vrací počet vteřin od 1.1.1900 00:00:00. Formáty reprezentace času, které se běžně používají v operačních systémech, počítají čas od 1.1.1970 00:00:00 (Unix epoch). Pro získání správného času je proto třeba odečíst od čísla, které vrací NTP server rozdíl mezi těmito dvěma časy, což je právě 2208988800 vteřin.

unsigned long cas = uchar(data[43]) + (uchar(data[42]) << 8) + (uchar(data[41]) << 16) + (uchar(data[40]) << 24);
cas -= 2208988800U;
QDateTime *time = new QDateTime::fromSecsSinceEpoch(cas);
qDebug() << time->toString("yyyy-MM-dd HH:mm:ss");

1)
Podrobnosti lze nalézt ve specifikaci protokolu.
courses/b2b99ppc/solutions/06.txt · Last modified: 2021/05/04 14:32 by viteks