#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;
#endifclass 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;
};
#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>
#endifNtpClient::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 表示 serverquint8 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 *)¤tLocalTimestamp, 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(); // 客户端接收到响应报文时的时间戳 T4if(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; // 原始时间戳 T1memcpy(&originTimestamp, buf.mid(24, 8), 8);qint64 receiveTimestamp = byte64ToMillionSecond(buf.mid(32, 8)) - unixDiff; // 接收时间戳 T2qint64 translateTimestamp = byte64ToMillionSecond(buf.mid(40, 8)) - unixDiff; // 传送时间戳 T3QDateTime dateTime;#if 0qDebug() << "-----------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() << "------------------------------------------";
#endifQString 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");
#endifqDebug() << strTime;setDateTime(dateTime);
}/*** @brief 设置系统时间(注意:这个功能需要使用管理员权限或者超级用户权限)* @param dateTime*/
void NtpClient::setDateTime(QDateTime& dateTime)
{QDate date = dateTime.date();QTime time = dateTime.time();
#ifdef Q_OS_WINSYSTEMTIME 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_LINUXstruct 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_sectv.tv_usec = time.msec() * 1000; // 设置微秒值if (0 == settimeofday(&tv, NULL)) // 仅限于超级用户, 使用sudo ./NtpClient{emit updateData("设置时间成功!");}else{emit updateData("设置时间失败!");}
#endif
}
QT4版本的:
头文件:
#ifndef NTPCLIENT_H
#define NTPCLIENT_H
#include <QUdpSocket>
#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;
#endifclass NtpClient : public QObject
{Q_OBJECT
public:explicit NtpClient(QObject *parent = NULL);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;
};#endif // NTPCLIENT_H
源文件:
#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>
#endifNtpClient::NtpClient(QObject *parent) : QObject(parent)
{m_socket = new QUdpSocket(this);connect(m_socket, SIGNAL(connected()), this, SLOT(on_connected()));connect(m_socket, SIGNAL(readyRead()), this, SLOT(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() << "**=======NTP SERVER CONNECT SUCCESS========***!";
// QMetaEnum m = QMetaEnum::fromType<QAbstractSocket::SocketState>(); // 获取QUdpSocket连接状态字符串 QT5
// emit updateData(QString("连接成功:%1 %2").arg(m_socket->peerName()).arg(m.key(m_socket->state())));QAbstractSocket::SocketState socketState = m_socket->state();const QMetaObject &mo = QAbstractSocket::staticMetaObject;QMetaEnum me = mo.enumerator(mo.indexOfEnumerator("SocketState"));qDebug()<<me.valueToKey(socketState);emit updateData(QString("连接成功:%1").arg(m_socket->peerName()))/*.arg(me.valueToKey(socketState)))*/;
}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 表示 serverquint8 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 *)¤tLocalTimestamp, 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();emit updateData(tr("发送NTP请求帧失败,请先连接服务器或检查连接线路.."));}
}/*** @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(); // 客户端接收到响应报文时的时间戳 T4if(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; // 原始时间戳 T1memcpy(&originTimestamp, buf.mid(24, 8), 8);qint64 receiveTimestamp = byte64ToMillionSecond(buf.mid(32, 8)) - unixDiff; // 接收时间戳 T2qint64 translateTimestamp = byte64ToMillionSecond(buf.mid(40, 8)) - unixDiff; // 传送时间戳 T3QDateTime dateTime,dateTimeDiff8;#if 1qDebug() << "-----------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() << "------------------------------------------";
#endifQString strTime;
#if 0 // 计算方式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");//UTC --> LOCALtimeQDateTime utc_from_str = QDateTime::fromString(strTime,Qt::ISODate);QDateTime utc_to_local = utc_from_str.toLocalTime();strTime = utc_to_local.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);dateTimeDiff8 = dateTime.addSecs(60*60*8); //解决时区问题加8个小时,个人平台问题加的,正常是不需要做处理的strTime = dateTimeDiff8.toString("yyyy-MM-dd HH:mm:ss zzz");
// //UTC --> LOCALtime
// QDateTime utc_from_str = QDateTime::fromString(strTime,Qt::ISODate);
// QDateTime utc_to_local = utc_from_str.toLocalTime();
// strTime = utc_to_local.toString("yyyy-MM-dd HH:mm:ss zzz");emit updateData(strTime);
#endifqDebug() << strTime;
// QString tmp = QString("date -s '%1'").arg(strTime);
// system(tmp.toLatin1().data());setDateTime(dateTimeDiff8);system("hwclock -w");system("sync");
}/*** @brief 设置系统时间(注意:这个功能需要使用管理员权限或者超级用户权限)* @param dateTime*/
void NtpClient::setDateTime(QDateTime& dateTime)
{QDate date = dateTime.date();QTime time = dateTime.time();
#ifdef Q_OS_WINSYSTEMTIME 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(tr("设置时间成功!"));}else{emit updateData(tr("设置时间失败!"));}
#endif#ifdef Q_OS_LINUXstruct 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_sectv.tv_usec = time.msec() * 1000; // 设置微秒值if (0 == settimeofday(&tv, NULL)) // 仅限于超级用户, 使用sudo{emit updateData(tr("设置时间成功!"));}else{emit updateData(tr("设置时间失败!"));}
#endif
}