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

一 、前言

    最近一段时间一直在弄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 表示 server
    quint8 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制作服务器端客户端的步骤: 1. 首先,需要安装Qt开发环境。可以从Qt官网下载安装包,然后按照提示进行安装。 2. 创建一个新的Qt项目,选择"Qt Widgets Application",然后在下一个页面中选择"Server",并设置项目名称和路径。 3. 在项目中添加服务器端代码。可以在Qt Creator中创建一个新的类,然后编写服务器端代码服务器端代码需要监听客户端的连接请求,接收客户端发送的消息,并将消息转发给其他客户端。 4. 使用Qt Designer来设计服务器端的界面。可以添加一些控件,如按钮、文本框等,用于显示服务器端的状态和接收客户端的输入。 5. 在服务器端代码中添加逻辑,将控件和代码进行连接。例如,当点击"启动服务器"按钮时,调用服务器端代码中的启动函数。 6. 创建一个新的Qt项目,选择"Qt Widgets Application",然后在下一个页面中选择"Client",并设置项目名称和路径。 7. 在项目中添加客户端代码。可以在Qt Creator中创建一个新的类,然后编写客户端代码客户端代码需要连接服务器端,并发送和接收消息。 8. 使用Qt Designer来设计客户端的界面。可以添加一些控件,如按钮、文本框等,用于显示客户端的状态和输入消息。 9. 在客户端代码中添加逻辑,将控件和代码进行连接。例如,当点击"连接服务器"按钮时,调用客户端代码中的连接函数。 10. 最后,在Linux环境下,需要使用命令行界面来测试这个即时聊天工具。可以打开多个终端窗口,分别运行服务器端和不同的客户端,并且在客户端之间发送消息进行测试。 以上是一个基本的实现方案,具体实现细节还需要根据具体的需求进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值