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 <QObject>
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

  • NtpClient.cpp
#include "ntpclient.h"
#include <QDateTime>
#include <QUdpSocket>
#include <QDebug>
#include <QtEndian>
#include <QElapsedTimer>
#include <QMetaEnum>

#ifdef Q_OS_WIN
#include <Windows.h>
#endif
#ifdef Q_OS_LINUX
#include <sys/time.h>
#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
}

4、源代码

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

posted @ 2022-08-25 22:55  mahuifa  阅读(0)  评论(0编辑  收藏  举报  来源