====== 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 [[https://cs.wikipedia.org/wiki/Network_Time_Protocol|NTP]] servery. NTP slouží pro synchronizaci hodin počítačů a využívá [[https://cs.wikipedia.org/wiki/User_Datagram_Protocol|UDP]] komunikační protokol na portu 123. Detaily o NTP protokolu a implementaci klienta v C lze najít [[https://lettier.github.io/posts/2016-04-26-lets-make-a-ntp-client-in-c.html|zde]]. Kompletní kód je ke stažení v [[https://gitlab.fel.cvut.cz/viteks/ppc/-/tree/master/tutorials/tut12/01-ntp-simple|gitlab]] repozitáři. ===== Vytvoření UDP klienta ===== Vytvoření UDP klienta je jednoduché a spočívá ve vytvoření instance třídy [[https://doc.qt.io/qt-5/qudpsocket.html|QUdpSocket]] a připojení k hostiteli. Pro práci s adresou hostitele využijeme třídu [[https://doc.qt.io/qt-5/qhostinfo.html|QHostInfo]] a připojíme se metodou [[https://doc.qt.io/qt-5/qabstractsocket.html#connectToHost-1|QAbstractSocket::connectToHost()]] k některému z časových serverů, které provozuje sdružení [[https://www.cesnet.cz/sluzby/casove-sluzby/|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 ''0x0B''((Podrobnosti lze nalézt ve [[https://tools.ietf.org/html/rfc1305|specifikaci protokolu]].)) 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 [[https://doc.qt.io/qt-5/qabstractsocket.html#waitForReadyRead|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 [[https://doc.qt.io/qt-5/qiodevice.html#read|QIODevice::read()]] nebo celou najednou metodou [[https://doc.qt.io/qt-5/qiodevice.html#readAll|QIODevice::readAll()]]. Zda jsou data ke čtení lze zjistit metodou [[https://doc.qt.io/qt-5/qudpsocket.html#hasPendingDatagrams|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 [[https://cs.wikipedia.org/wiki/Endianita#Big-endian|big endian]] (network byte order), ale formát dat klienta bude velmi pravděpodobně [[https://cs.wikipedia.org/wiki/Endianita#Little-endian|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 [[https://linux.die.net/man/3/ntohl|ntohl()]], která je v OS Windows součástí knihovny [[https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-ntohl|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 ([[https://cs.wikipedia.org/wiki/Unixov%C3%BD_%C4%8Das|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");