当前位置: 首页> 科技> 互联网 > 基于QT的NTP客户端和服务器端协议和代码详解(1)

基于QT的NTP客户端和服务器端协议和代码详解(1)

时间:2025/7/11 15:08:22来源:https://blog.csdn.net/weixin_45426095/article/details/142180517 浏览次数:0次

一 、前言

    最近一段时间一直在弄NTP协议。本来需要做NTP的服务器端程序,但是由于不知道自己编写的代码能不能运行,所以还需要编写NTP客户端程序来验证。最后决定客户端和服务器端程序都需要编写。

二、现有问题  

     前期在网上搜索了很多相关资料,有写NTP客户端程序的,有写NTP服务器端程序的,有介绍NTP协议的。发现问题如下:

(1)客户端方面:程序中仅仅介绍发送和接收方式,却没有介绍怎么设定相关参数。协议方面也仅仅介绍了各个报文对应什么含义,具体怎么填充却没有详细介绍;

(2)服务器端方面:程序很少,并且也没有介绍参数怎么设定,协议方面也和客户端协议一样,对具体数据填充都仅仅停留在含义方面。

三、解决办法

    在现有资料的基础上,首先解决客户端程序问题,并且多次尝试程序中具体数据代表意义,并且发送到实际网络中,分析发送报文的可能性和接收报文的可能性,确定发送报文的正确性和接收报文的解析。再次解决服务器端程序问题,分析接收报文和回复报文的具体含义,并且通过已经验证的客户端程序和报文进行模拟,同时接入现有时钟装置进行实际验证,确定接收报文的解析和发送报文的正确性。

    前提条件:(1)网络NTP,可以验证客户端的正确性;(2)自我收发NTP,可以验证客户端和服务器端的互联及模拟;(3)时钟同步装置,可以验证服务器的正确性。

四、客户端

1.协议

    先说下需要填充的量:前置信息(必须)、标识符(非必须)、传送时间戳(必须)。

(1)前置信息:闰秒,版本,模式,层,测试间隔,精度。这几个全部测试过了:

    闰秒主要针对服务器端回复信息,与客户端的填充没有什么关系,所以可以不填充,那就填0,如果你想填其他的也可以,和服务器端回复信息没什么关系。

    版本号也测试过了,是兼容的,客户端填1,2,3都可以,服务器回复的时候都是回复3。应该网上的NTP服务器都是用的3版本。所以填充最新的3就行。网上有的写返回的是最新的版本4,但是我连接下所有的网络NTP服务器,返回的都是3。

    模式这个是必须填写3,因为是客户端,这个是必须固定的,其他的会出现意想不到的问题。服务器回复的时候回复4。也就是说这个是发送信息者的信息,所以对应填充就行。

    层这个随意,这个定义只针对服务器端的回复报文,所以也都填充成0。

    测试间隔和精度这个也随意,改变之后不改变服务器端回复的报文,所以也都填充成0。

(2)标识符,这个很有意思,这个发送的时候,对于网络NTP服务器没有任何影响。改变之后不改变服务器端回复信息。所以这个也是随意的。但是如果有特殊要求的话,需要填入对应字符。

(3)传送时间戳。这个也测试了下,只有填写到最后的位置才能让服务器端返回正确的数据。参考所有网上写的,都是说原始时间戳是客户端发送的时间,其实这个是针对服务器端返回的报文,却不针对客户端报文。其实正确的理解方式应该是,传送时间戳是离开客户端和服务器端的时间,不管是客户端还是服务器端都是要填充这个数据。客户端数据发送时间填充到这里,服务器端数据发送时间也填充到这里,而不是协议里面写的填充到原始时间戳那里。

2.报文解析

    客户端发送报文举例如下:

1B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E9 D4 0C E9 91 01 00 00

    具体分析下:

1B:0001 1011       00代表无闰秒预告。011代表NTP版本号3。011代表客户端模式。最好这样填充,以免出现兼容问题。

E9 D4 0C E9 91 01 00 00  这个是客户端的传送时间戳。有个问题需要注意,NTP采用的是小端模式,具体为啥不清楚,所以解析的时候必须反过来,也就是:00 00 01 91 E9 0C D4 E9,这个才是真正的时间戳,转化为十进制:1726191817961。这里需要注意:1726191817是秒数,961是毫秒数。利用时间戳转换工具和自己编写的软件转换时间:

    所以这种解析方式是正确的,并且只能这样填充。所以填充步骤:先获得时间戳,记得是毫秒戳,然后转化为8个字节的数据,然后大小端互换,然后填充进去。

其他的都填充0就可以,至于想试试的小伙伴可以全部都试试。基本的格式是第1个字节和最后8个字节必须填充好,这样可以从网络NTP服务器中获得正确的时间。

3.程序

    程序编写前提:对QT掌握的熟练,基本的操作是必须得。前面的文章有介绍QT的基本操作,可以看看。对于基本的操作部分仅仅贴出代码,主要介绍和NTP客户端相关的程序。

    这篇文章仅仅介绍NTP客户端的发送程序,至于接收的报文,下篇文章再介绍。

    这个是客户端的界面。程序里面都是根据汉字意思翻译过去的变量,所以应该挺好理解。

    注:编写有交互的软件最好是发送报文和接收报文都显示,这样方便用现有的软件对程序进行调试。这个前期我采用网络调试助手中的UDP去的调试的。很方便。

    程序应该添加什么头文件,这个是必须得。什么pro里面添加network,头文件添加:

#include  <QUdpSocket>

这些都是基本操作,就不详细解释了。

初始化阶段程序,形成界面,至于界面做成什么样子,自己修改就行,这里重复代码比较多,所以仅仅是给出个大概。

ntpclient::ntpclient(QWidget *parent) :QWidget(parent),ui(new Ui::ntpclient)
{ui->setupUi(this);m_socket=new QUdpSocket(this);connect(m_socket, &QUdpSocket::readyRead, this, &ntpclient::on_readData);connect(ui->connect,&QPushButton::clicked,this, &ntpclient::myconnect);connect(ui->send,&QPushButton::clicked,this, &ntpclient::sendData);model = new QStandardItemModel();ui->tableView->setModel(model);ui->tableView->verticalHeader()->hide();ui->tableView->horizontalHeader()->hide();ui->tableView->horizontalHeader()->setStretchLastSection(true);ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);ui->tableView->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);for (int i=0;i<10;i++){const QStringList rows[] ={QStringList{QString::number(0),QString::number(0),QString::number(0),QString::number(0)}};for (const QStringList &row : rows){items.clear();for (const QString &text : row){items.append(new QStandardItem(text));}model->appendRow(items);}}uint8_t row;row = model->rowCount();for (int i=0;i<row;i++){model->item(i,0)->setTextAlignment(Qt::AlignCenter);model->item(i,1)->setTextAlignment(Qt::AlignCenter);model->item(i,2)->setTextAlignment(Qt::AlignCenter);model->item(i,3)->setTextAlignment(Qt::AlignCenter);}model->item(0,0)->setText("闰秒");model->item(0,0)->setBackground(QColor("LightGray"));ui->tableView->setStyleSheet("gridline-color: rgb(0, 0, 0)");}

当然对应h文件里面也需要添加对应变量,放进去之后自己调试下就行,相关基本操作就不再一一说了,熟悉QT的应该挺容易理解的。

 #include <QUdpSocket>#include <QStandardItemModel>   QUdpSocket *m_socket;QByteArray toNtpPacket();void myconnect();void connectServer(QString url);        // 连接Ntp服务void close();QStandardItemModel *model;QList<QStandardItem *> items;

这样基本的工作就做好了。下面介绍和NTP客户端相关的发送程序。

第一个:连接NTP服务器。这个包含2个:自己的服务器,网络服务器。

void ntpclient::myconnect()
{connectServer("ntp.tencent.com");//    connectServer("127.0.0.1");
}
void ntpclient::connectServer(QString url)
{m_socket->connectToHost(url, 123);if(m_socket->waitForConnected()){qDebug() << "连接成功";}else{qDebug() << "连接失败";}
}

    主要两个参数:IP需要改变。PORT是固定的123。

    网络NTP服务器,那么填入需要的网站,这个搜素的话有很多,选一个能用的。注意:腾讯的比较快,其他的都测试了都比较慢,并且有的连接不上,所以最好选用这个,测试方便。端口号一定是123。这个是应规定,NTP服务器就是用的这个端口号。

    自己的服务器:这个又分为两种,同一台电脑上,两台电脑上。

    同一台电脑:IP是127.0.0.1。为啥是这个,我用网络助手和自己最后编程的软件都测试过了。自己的电脑是自己虚拟自己的IP地址,如果采用同一台电脑模拟客户端和服务器端的话,IP就是

这个。客户端和服务器端都是这个,并且这个端口他也会自己虚拟出来一个端口用。所以这个没办法。

    两台电脑上:一台电脑上客户端,另一台电脑服务器端。这样这样IP就是自己的物理地址。注意:由于采用UDP模式,两台电脑可以用网线直接互联,这个测试过了,不需要路由器啥的,直接网线怼一起就行。

    前期没有服务器端代码,只能采用网络调试助手去调试,互联一下,看看网络助手能不能接收到数据,能接收到就行,乱码也行,说明路通了。然后再一步一步的修改代码,直至得到正确的数据。

第二个:填充报文。

QByteArray ntpclient::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 = 0;                 // 轮询时间,即两个连续NTP报文之间的时间间隔(4-14)qint8 precision = 0;             // 系统时钟的精度,精确到秒的平方级(-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));qDebug()<<currentLocalTimestamp;return result;
}

    里面填充和前面介绍的一样。为了方便取了40个数据,后面直接append,就把时间戳给加上了。里面的QDateTime::currentMSecsSinceEpoch()已经将数据转化好了,毫秒戳,大小端互换,所以直接填充就好。

    题外话:这个数据就解析的很烦躁,不知道他的解析规则,只能自己猜,为啥,为啥,为啥又不对。最后找了很多文章才发现是大小端互换,并且后面是3位毫秒。所以数据出来的时候都不知道为啥是这个。为啥和网络的时间戳对不上,怎么都解析不出来。最后才发现是这样解析的。

第三个:发送报文

void ntpclient::sendData()
{QByteArray arr = toNtpPacket();qint64 len = m_socket->write(arr);if(len != arr.count()){qWarning() << "发送NTP请求帧失败:" << arr.toHex(' ');}uint8_t gnReceiveData[48];ui->textEdit_2->clear();for(int i=0; i<48; i++){gnReceiveData[i] = arr[i];ui->textEdit_2->insertPlainText(QString("%1").arg(gnReceiveData[i],2,16,QLatin1Char('0')).toUpper());ui->textEdit_2->insertPlainText(" ");}
}

    直接发送字符,然后在ui界面显示发送报文。

至此客户端发送程序完毕。

五、写在最后

    协议这东西,得慢慢的磨,没办法的事情。有些如果别人写的不清楚就得自己慢慢的去测试,直到测试成功的时候。期间会很烦躁,怎么这不对,怎么那不对。最后还是得静下心去慢慢搞。当然如果赶的特别紧的话,就很烦人了。有些东西是快不得的,还是得一步一步按照自己的节奏了。

    协议都不难,理解了都没啥问题,程序也应该没啥问题。可以说这部分是时间活,是繁琐点,而不是难点。最重要的是静下心来弄。如果时间不够,那就没办法,只能加班了。

    写完文章之后会将代码放进公众号里面,由于现在不能贴公众号的二维码,所以只能翻看以前的文章去找二维码。需要的小伙伴可以关注喜爱,将来会放在百度网盘里自行下载。

关键字:基于QT的NTP客户端和服务器端协议和代码详解(1)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: