• QtNtp时间同步客户端


    QtNtp时间同步客户端

    更多精彩内容
    👉个人内容分类汇总 👈

    1、概述

    • Qt版本:V5.12.5
    • Ntp协议

    NTP(Network Time Protocol,网络时间协议)是由RFC 1305定义的时间同步协议,用来在分布式时间服务器和客户端之间进行时间同步。NTP基于UDP报文进行传输,使用的UDP端口号为123

    使用NTP的目的是对网络内所有具有时钟的设备进行时钟同步,使网络内所有设备的时钟保持一致,从而使设备能够提供基于统一时间的多种应用。

    对于运行NTP的本地系统,既可以接收来自其他时钟源的同步,又可以作为时钟源同步其他的时钟,并且可以和其他设备互相同步。

    • NTP时间同步客户端程序说明
    1. 使用UDP进行通信;
    2. 毫秒级时间精度;
    3. 使用多个阿里云NTP时间同步服务器、腾讯云NTP时间同步服务器;
    4. 支持windows、linux下修改系统时间。

    注意: 由于设置系统时间的功能比较重要,所以不管是Windows还是Linux都需要最高权限才可以。

    1. Windows下需要【以管理员身份运行】打开QtCreator或者编译后给NtpClient.exe设置权限【属性->兼容性->以管理员身份运行此程序】,否则无法修改系统时间;
    2. Linux下编译后使用【sudo ./NtpClient】 运行程序。

    2、实现效果

    在这里插入图片描述

    3、关键代码

    • NtpClient.h
    #ifndef NTPCLIENT_H
    #define NTPCLIENT_H
    
    #include 
    class QUdpSocket;
    
    #if 0   // NTP协议帧(未使用)
    typedef struct
    {
        char  LI_VN_Mode;
        char  Stratum;
        char  Poll;
        char  Precision;
        int  RootDelay;
        int  RootDispersion;
        int  ReferenceIdentifier;
        quint64  ReferenceTimeStamp;    // 系统时钟最后一次被设定或更新的时间
        quint64   OriginateTimeStamp;    // NTP请求报文离开发送端时发送端的本地时间
        quint64   ReceiveTimeStamp;      // NTP请求报文到达Server端时接收端的本地时间。
        quint64   TransmitTimeStamp;     // 发送时间戳,客户端发送时填写,server接收到后会将TransmitTimeStamp值写入OriginateTimeStamp,然后NTP应答报文离开Server时在OriginateTimeStamp的本地时间。
    }NtpPacket;
    #endif
    
    class NtpClient : public QObject
    {
        Q_OBJECT
    public:
        explicit NtpClient(QObject *parent = nullptr);
    
        void connectServer(QString url);        // 连接Ntp服务
        void close();
        void getTime();
    
    signals:
        void updateData(const QString& time);          // 添加显示到界面上文本框中的信息
    
    private slots:
        void on_connected();
        void on_readData();
        void sendData();
        void setDateTime(QDateTime& dateTime);
    
    private:
        QUdpSocket* m_socket = nullptr;
    };
    
    #endif // NTPCLIENT_H
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • NtpClient.cpp
    #include "ntpclient.h"
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #ifdef Q_OS_WIN
    #include 
    #endif
    #ifdef Q_OS_LINUX
    #include 
    #endif
    
    NtpClient::NtpClient(QObject *parent) : QObject(parent)
    {
        m_socket = new QUdpSocket(this);
        connect(m_socket, &QUdpSocket::connected, this, &NtpClient::on_connected);
        connect(m_socket, &QUdpSocket::readyRead, this, &NtpClient::on_readData);
    }
    
    /**
     * @brief        连接Ntp服务器,端口号默认123
     * @param url    Ntp服务器IP地址或网址
     */
    void NtpClient::connectServer(QString url)
    {
        close();
        m_socket->connectToHost(url, 123);
    }
    
    void NtpClient::close()
    {
        m_socket->abort();
    }
    
    void NtpClient::on_connected()
    {
        qDebug() << "连接成功!";
        QMetaEnum m = QMetaEnum::fromType<QAbstractSocket::SocketState>();      // 获取QUdpSocket连接状态字符串
        emit updateData(QString("连接成功:%1  %2").arg(m_socket->peerName()).arg(m.key(m_socket->state())));
    }
    
    void NtpClient::getTime()
    {
        sendData();
    }
    
    QByteArray toNtpPacket() {
        QByteArray result(40, 0);
    
        quint8 li = 0;                   // LI闰秒标识器,占用2个bit,0 即可;
        quint8 vn = 3;                   // VN 版本号,占用3个bits,表示NTP的版本号,现在为3;
        quint8 mode = 3;                 // Mode 模式,占用3个bits,表示模式。 3 表示 client, 2 表示 server
        quint8 stratum = 0;              // 系统时钟的层数,取值范围为1~16,它定义了时钟的准确度。层数为1的时钟准确度最高,准确度从1到16依次递减,层数为16的时钟处于未同步状态,不能作为参考时钟。
        quint8 poll = 4;                 // 轮询时间,即两个连续NTP报文之间的时间间隔(4-14)
        qint8 precision = -6;            // 系统时钟的精度,精确到秒的平方级(-6 到 -20)
    
        result[0] = char((li << 6) | (vn <<3) | (mode));
        result[1] = char(stratum & 0xff);
    
        result[2] = char(poll & 0xff);
        result[3] = char(precision & 0xff);
    
        qint64 currentLocalTimestamp = QDateTime::currentMSecsSinceEpoch();
        result.append((const char *)&currentLocalTimestamp, sizeof(qint64));
    
        return result;
    }
    
    /**
     * @brief 发送NTP请求帧
     */
    void NtpClient::sendData()
    {
        QByteArray arr = toNtpPacket();
        qint64 len = m_socket->write(arr);
        if(len != arr.count())
        {
            qWarning() << "发送NTP请求帧失败:" << arr.toHex(' ');
        }
    }
    
    
    /**
     * @brief     将QByteArray类型时间戳数据转换为整形并且进行大小端转换
     * @param bt
     * @return
     */
    quint32 byteToUInt32(QByteArray bt) {
        if(bt.count() != 4) return 0;
    
        quint32 value;
        memcpy(&value, bt.data(), 4);
    
        return qToBigEndian(value);       // 大端转小端
    }
    
    /**
     * @brief      将Ntp时间戳转换成QDateTime可用的时间戳
     * @param bt
     * @return
     */
    qint64 byte64ToMillionSecond(QByteArray bt) {
        qint64 second = byteToUInt32(bt.left(4));
        qint64 millionSecond = byteToUInt32(bt.mid(4, 4));
        return (second * 1000L) + ((millionSecond * 1000L) >> 32);
    }
    
    /**
     * @brief 接收返回的NTP数据帧并解析
     */
    void NtpClient::on_readData()
    {
        QElapsedTimer timer;       // 统计数据解析消耗的时间
        timer.start();
    
        QByteArray buf = m_socket->readAll();
        qint64 currentLocalTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch();        // 客户端接收到响应报文时的时间戳 T4
        if(buf.count() < 48)          // Ntp协议帧长度为48字节
        {
            return;
        }
    
        QDateTime epoch(QDate(1900, 1, 1), QTime(0, 0, 0));           // ntp时间计时从1900年开始
        QDateTime unixStart(QDate(1970, 1, 1), QTime(0, 0, 0));       // UNIX操作系统考虑到计算机产生的年代和应用的时限综合取了1970年1月1日作为UNIX TIME的纪元时间(开始时间)
        qint64 unixDiff = epoch.msecsTo(unixStart);
    
        // 解析ntp协议中的时间
        qint64 referenceTimestamp = byte64ToMillionSecond(buf.mid(16, 8)) - unixDiff;           // 参考时间戳
        qint64 originTimestamp;                                                                 // 原始时间戳    T1
        memcpy(&originTimestamp, buf.mid(24, 8), 8);
        qint64 receiveTimestamp = byte64ToMillionSecond(buf.mid(32, 8)) - unixDiff;             // 接收时间戳   T2
        qint64 translateTimestamp = byte64ToMillionSecond(buf.mid(40, 8)) - unixDiff;           // 传送时间戳   T3
    
        QDateTime dateTime;
    
    #if 0
        qDebug() << "-----------NTP协议中包含的所有时间-----------";
        dateTime.setMSecsSinceEpoch(referenceTimestamp);
        qDebug() << "参考时间戳:  " << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        dateTime.setMSecsSinceEpoch(originTimestamp);
        qDebug() << "原始时间戳T1:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        dateTime.setMSecsSinceEpoch(receiveTimestamp);
        qDebug() << "接收时间戳T2:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        dateTime.setMSecsSinceEpoch(translateTimestamp);
        qDebug() << "传送时间戳T3:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        dateTime.setMSecsSinceEpoch(currentLocalTimestamp);
        qDebug() << "本地时间戳T4:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        qDebug() << "------------------------------------------";
    #endif
    
        QString strTime;
    #if 1         // 计算方式1:时间差offset=((T2-T1)+(T3-T4))/2      实际时间=程序处理时间(timer.elapsed()) + 接收数据时间T4 + 客户端与服务端的时间差(offset)
        qint64 currentLocalTimestamp1 = timer.elapsed() + currentLocalTimestamp + qint64((receiveTimestamp - originTimestamp + translateTimestamp - currentLocalTimestamp) / 2);
    
        dateTime.setMSecsSinceEpoch(currentLocalTimestamp1);
        strTime = dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
        emit updateData(strTime);
    #else        // 计算方式2:往返时延Delay=(T4-T1)-(T3-T2)            实际时间=程序处理时间(timer.elapsed()) + 服务器数据发出时间(T3)+ 通信时延(Delay)
        qint64 currentLocalTimestamp2 = timer.elapsed() + translateTimestamp + (((currentLocalTimestamp - originTimestamp) - (translateTimestamp - receiveTimestamp)) / 2);
        dateTime.setMSecsSinceEpoch(currentLocalTimestamp2);
        strTime = dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
    #endif
        qDebug() << strTime;
        setDateTime(dateTime);
    }
    
    
    /**
     * @brief           设置系统时间(注意:这个功能需要使用管理员权限或者超级用户权限)
     * @param dateTime
     */
    void NtpClient::setDateTime(QDateTime& dateTime)
    {
        QDate date = dateTime.date();
        QTime time = dateTime.time();
    #ifdef Q_OS_WIN
    
        SYSTEMTIME system_time = {0};
        memset(&system_time, 0, sizeof(SYSTEMTIME));
        system_time.wYear = date.year();
        system_time.wMonth = date.month();
        system_time.wDay = date.day();
        system_time.wHour = time.hour();
        system_time.wMinute = time.minute();
        system_time.wSecond = time.second();
        system_time.wMilliseconds = time.msec();
        if (SetLocalTime(&system_time))            // 仅限于管理员。
        {
            emit updateData("设置时间成功!");
        }
        else
        {
            emit updateData("设置时间失败!");
        }
    #endif
    
    #ifdef Q_OS_LINUX
        struct tm tptr;
        struct timeval tv;
    
        tptr.tm_year = date.year() - 1900;            // 这里必须-1900,否则设置不成功
        tptr.tm_mon = date.month() - 1;               // [0-11]
        tptr.tm_mday = date.day();
        tptr.tm_hour = time.hour();
        tptr.tm_min = time.minute();
        tptr.tm_sec = time.second();
    
        tv.tv_sec = mktime(&tptr);                    // 将tptr赋值给tv_sec
        tv.tv_usec = time.msec() * 1000;              // 设置微秒值
    
        if (0 == settimeofday(&tv, NULL))            // 仅限于超级用户, 使用sudo ./NtpClient
        {
            emit updateData("设置时间成功!");
        }
        else
        {
            emit updateData("设置时间失败!");
        }
    #endif
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223

    4、源代码

    💡💡💡💡💡💡💡💡💡💡💡💡💡💡

  • 相关阅读:
    python系列笔记——容器、序列(列表、元组、字符串)、集合和映射(字典)面试常考知识点汇总
    经典/最新计算机视觉论文及代码推荐
    openEuler 22.03 LTS SP3(华为欧拉)一键安装 Oracle 11GR2 RAC(231017)
    测试人生 | 97年双非学历的小哥哥,2线城市涨薪100%,我酸了......
    Java Spring拦截器优化实践: 专注于API路径拦截
    八大排序算法-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(下)
    气传导耳机有哪些品牌?性能不错的气传导耳机分享
    Vue+Element Progress 进度条显示文字 %修改,使用format方法显示文字可自定义
    用Java实现Nginx插件
    矩阵中幸运数(Python)
  • 原文地址:https://blog.csdn.net/qq_43627907/article/details/126534158