C++使用Boost實現Network Time Protocol(NTP)客戶端

html

        筆者機器上安裝了兩個系統,一個Linux Ubuntu,一個Windows8.1。讓人感到鬱悶的是,每次從Ubuntu重啓進入Windows時,系統時間老是少了8個小時,每次都要用Windows的時間程序進行同步,也就是下面這個東西:
ios

這個東西其實就是一個NTP Client,從Internet上選擇一臺NTP Server,獲取UTC時間,而後設置本地時間。編程

因而我想本身實現一個這樣的程序,先百度一下吧,網上有不少關於NTP的資料和實現代碼,大可能是單一平臺的,不能跨平臺api

,下面給幾個參考:網絡

http://blog.csdn.net/loongee/article/details/24271129 socket

http://blog.csdn.net/chexlong/article/details/6963541 函數

http://www.cnblogs.com/TianFang/archive/2011/12/20/2294603.html ui

本文使用boost的Asio來跨平臺實現NTP Client.編碼

準備spa

1. 最新的boost庫,本文使用的是1.56.0版本

        要用到裏面的ASIO網絡庫

2. IDE是Visual Studio 2013 with Update3

        筆者是版本帝

3. WireShark也是最新的1.12.1版本

        用來分析Windows自帶的NTP Client

NTP Packet分析

        這裏咱們分析的正是上圖那個程序,點擊當即更新,會發送NTP的請求包,下面是Wireshark的抓包結果:

能夠獲得下面一些信息:

  1. NTP時間同步分兩個過程,一個Request,一個Response

  2. 這裏的NTP Server的IP地址是129.6.15.28

  3. 程序沒有進行DNS解析,多是直接保存了IP地址

  4. NTP服務的端口號是123,Client也使用了123端口,後來發現Client不是必定要使用123端口的

  5. NTP協議是構建在UDP傳輸協議上的應用協議

  6. 這裏使用V3版的NTP協議,目前還有v4

好了,有了關於NTP協議的一些基本信息,咱們再來看看應用層的詳細信息:

Response包:

分了不少字段,關於每一個字段的含義請參考上面給出的連接,本文主要講實現。這裏Reference Timestamp就是Request包發送的Timestamp,而Origin,Receive,Transmit都是從Server返回回來的時間,後三個時間都相差很是小,所以方便一點,咱們取最後一個Transmit Timestamp做爲結果。

編碼

boost裏面相關庫的編譯能夠參考官方的文檔,裏面有很是簡單的例子。

1. 須要的頭文件和名字空間

#include <iostream>
#include "boost/asio.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"

using namespace boost::posix_time;
using namespace boost::asio::ip;

2. NtpPacket的構造

class NtpPacket {
public:
    NtpPacket() {
        _rep._flags = 0xdb;
        //    11.. ....    Leap Indicator: unknown
        //    ..01 1...    NTP Version 3
        //    .... .011    Mode: client
        _rep._pcs = 0x00;//unspecified
        _rep._ppt = 0x01;
        _rep._pcp = 0x01;
        _rep._rdy = 0x01000000;//big-endian
        _rep._rdn = 0x01000000;
        _rep._rid = 0x00000000;
        _rep._ret = 0x0;
        _rep._ort = 0x0;
        _rep._rct = 0x0;
        _rep._trt = 0x0;
    }

    friend std::ostream& operator<<(std::ostream& os, const NtpPacket& ntpacket) {
        return os.write(reinterpret_cast<const char *>(&ntpacket._rep), sizeof(ntpacket._rep));
    }

    friend std::istream& operator>>(std::istream& is, NtpPacket& ntpacket) {
        return is.read(reinterpret_cast<char*>(&ntpacket._rep), sizeof(ntpacket._rep));
    }

public:
#pragma pack(1)
    struct NtpHeader {
        uint8_t _flags;//Flags
        uint8_t _pcs;//Peer Clock Stratum
        uint8_t _ppt;//Peer Polling Interval
        uint8_t _pcp;//Peer Clock Precision
        uint32_t _rdy;//Root Delay
        uint32_t _rdn;//Root Dispersion
        uint32_t _rid;//Reference ID
        uint64_t _ret;//Reference Timestamp
        uint64_t _ort;//Origin Timestamp
        uint64_t _rct;//Receive Timestamp
        uint64_t _trt;//Transmit Timestamp
    };
#pragma pack()
    NtpHeader _rep;
};

這裏爲了方便存取就沒有把struct放到private中,須要注意的是結構體各個字段的順序和須要進行內存1字節對齊,即便用:

#pragma pack(1)

內存對齊在網絡編程中十分重要,他會直接影響Packet的內容,關於內存對齊能夠參考:

http://www.cppblog.com/cc/archive/2006/08/01/10765.html

NTP請求包中最重要的是flags,裏面存有版本信息等直接影響協議工做的內容,所以不能搞錯了。

兩個operator重載用來方便讀寫Packet數據。

再來看看Client類的實現,Client類的主要任務就是發送和接受NTP包,並返回最後那個64bit的Timestamp。

class NtpClient {
public:
    NtpClient(const std::string& serverIp)
        :_socket(io), _serverIp(serverIp) {
    }

    time_t getTime() {
        if (_socket.is_open()) {
            _socket.shutdown(udp::socket::shutdown_both, _ec);
            if (_ec) {
                std::cout << _ec.message() << std::endl;
                _socket.close();
                return 0;
            }
            _socket.close();
        }
        udp::endpoint ep(boost::asio::ip::address_v4::from_string(_serverIp), NTP_PORT);
        NtpPacket request;
        std::stringstream ss;
        std::string buf;
        ss << request;
        ss >> buf;
        _socket.open(udp::v4());
        _socket.send_to(boost::asio::buffer(buf), ep);
        std::array<uint8_t, 128> recv;
        size_t len = _socket.receive_from(boost::asio::buffer(recv), ep);
        uint8_t* pBytes = recv.data();
        /****dump hex data
        for (size_t i = 0; i < len; i++) {
        if (i % 16 == 0) {
        std::cout << std::endl;
        }
        else {
        std::cout << std::setw(2) << std::setfill('0')
        << std::hex << (uint32_t) pBytes[i];
        std::cout << ' ';
        }
        }
        ****/
        time_t tt;
        uint64_t last;
        uint32_t seconds;
        /****get the last 8 bytes(Transmit Timestamp) from received packet.
        std::memcpy(&last, pBytes + len - 8, sizeof(last));
        ****create a NtpPacket*/
        NtpPacket resonpse;
        std::stringstream rss;
        rss.write(reinterpret_cast<const char*>(pBytes), len);
        rss >> resonpse;
        last = resonpse._rep._trt;
        //
        reverseByteOrder(last);
        seconds = (last & 0x7FFFFFFF00000000) >> 32;
        tt = seconds + 8 * 3600 * 2 - 61533950;
        return tt;
    }

private:
    const uint16_t NTP_PORT = 123;
    udp::socket _socket;
    std::string _serverIp;
    boost::system::error_code _ec;
};

注意幾個地方:

1. udp::socket是boost裏面使用udp協議的套接字,他的構造須要一個io_service,io_service能夠直接在全局區進行聲明:

boost::asio::io_service io;

2. 建立一個endpoint用來表示NTP Server的地址:

udp::endpoint ep(boost::asio::ip::address_v4::from_string(_serverIp), NTP_PORT);

向這個ep send_to,並從這個ep receive_from數據包。

3. time_t的定義以下:

typedef __time64_t time_t;      /* time value */
typedef __int64 __time64_t;     /* 64-bit time value */

也就是說這個time_t其實就是一個64bit的int,咱們能夠用uint64_t這個類型與之互換,他能夠用來表示一個Timestamp。

4. 獲取最後8字節內容有兩種方式,一種是直接複製pBytes的內存,一種是構造NtpPacket,而後取成員,這裏選擇後者易於理解。

5. 字節序的問題

網絡字節序都是大端模式,須要進行轉換,因爲僅僅須要最後那個uint64_t因此我寫了一個針對64bit的字節序轉換函數:

static void reverseByteOrder(uint64_t &in) {
    uint64_t rs = 0;
    int len = sizeof(uint64_t);
    for (int i = 0; i < len; i++) {
        std::memset(reinterpret_cast<uint8_t*>(&rs) + len - 1 - i
                    , static_cast<uint8_t> ((in & 0xFFLL << (i * 8)) >> i * 8)
                    , 1);
    }
    in = rs;
}

最後一個64bit內容的高32位存了UTC秒數,因此須要取出來,而後再轉換爲本地時區的秒數。

seconds = (last & 0x7FFFFFFF00000000) >> 32;

注意最高位是不能取的,儘管是unsigned,至於爲何要- 61533950這個是筆者在本身電腦上嘗試出來的,找了不少資料不

知是哪裏的問題,還請各位知道的讀者告訴我哈。

再來看看主函數:

int main(int argc, char* agrv[]) {
    NtpClient ntp("129.6.15.28");
    int n = 5;
    while (n--) {
        time_t tt = ntp.getTime();
        boost::posix_time::ptime utc = from_time_t(tt);
        std::cout << "Local Timestamp:" << time(0) << '\t' << "NTP Server:" << tt << "(" << to_simple_string(utc) << ")" << std::endl;
        Sleep(10);
    }
    return 0;
}

這裏進行5次NTP請求,並使用boost的to_simple_string轉換UTC時間打印結果。

大概是這種效果:

收尾

        同步時間通常都會想到找一個http api接口,本文主要是用了NTP協議。爲了跨平臺,上面的代碼儘量避免使用平臺相關的宏和函數,只要稍做修改就能在各類平臺下執行,也得益於boost這個強悍的準標準庫給開發者帶來的便利。

相關文章
相關標籤/搜索