小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
Qt系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【Qt】系统相关(九)TCP回显服务器的实现,QTcpServer和QTcpSocket的讲解——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【Qt】系统相关(十)TCP回显客户端的实现,测试


一、TCP回显客户端的实现

  1. 在上一篇文章中,小编讲解了TCP相关接口以及TCP回显服务器,本文TCP回显客户端与上一篇文章强相关,所以对上一篇不了解的读者友友务必点击后方蓝字链接进行学习 详情请点击<——
  2. 同样的TCP回显客户端的实现和UDP回显客户端的项目布置,项目的运行,界面的布局基本都是一致的,小编对于这些不会二次讲解,所以对于UDP回显服务器不太了解的读者友友请点击后方蓝字链接进行学习 详情请点击<——
    在这里插入图片描述
  3. 为了便于支持我们后续进行服务器和客户端的测试,所以我们需要服务器和客户端的项目代码同时被打开,Qt Creator中是可以同时打开多个项目的,所以在TCP回显服务器对应的TcpServer项目的基础上,所以接下来我们再创建一个项目名为TcpClient,基类为QWidget,派生类为Widget的项目
    在这里插入图片描述
  4. 那么对于TCP回显客户端对应的TcpClient项目,TCP回显客户端要支持图形化便于用户操作,应该支持用户进行输入,所以要有一个单行输入框,同样还要给用户提供一个发送按钮,同样还要有一个列表用于显示客户端对应的的请求信息,服务器对应的响应信息,用户在单行输入框中进行输入请求信息,点击发送的按钮之后,类似于微信聊天一样发送了消息此时单行输入框就要被清空
  5. 点击按钮之后,客户端的请求信息封装成请求报文就要发送给服务器,接下来将客户端的请求信息作为一个元素回显到列表上,服务器收到了来自客户端的请求报文之后,构建好响应报文发送给客户端,客户端收到来自服务器的相应报文之后,将响应报文中的响应信息回显作为一个元素回显到列表上即可,所以对于TCP回显客户端对应的TcpClient项目,接下来我们点击ui文件,进入Qt Designer
    在这里插入图片描述
  6. 所以此时我们拖拽左侧红框内的控件,然后调整成上图界面即可,objectName保持不变,关于如何进行调整布局,小编在UDP客户端的实现中已经进行了详尽的讲解,这里就不再赘述了 关于UDP客户端的实现,详情请点击<——,接下来我们右击按钮,然后点击转到槽,接下来我们选择clicked信号,让Qt帮我们生成对应槽函数的声明和定义
    在这里插入图片描述
  7. 那么接下来的初步流程和TCP回显器中的一致,由于TCP回显客户端需要使用Qt中网络模块的功能,所以我们在.pro文件中的第一行加入network引入Qt网络模块的功能,然后左下角点击运行,让Qt解析.pro文件的内容,引入Qt网络模块的功能,确保我们后面可以正确包含TCP的相关头文件
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpSocket>


QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;

    QTcpSocket* socket;
};
#endif // WIDGET_H

  1. 由于我们要使用TCP客户端和服务器建立建立,进行通信,读写数据,所以此时我们需要在Widget的.h头文件中声明一个QTcpSocket*类型的socket对象
#include "widget.h"
#include "ui_widget.h"
#include <QHostAddress>
#include <QMessageBox>


Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->setWindowTitle("客户端");

    socket = new QTcpSocket(this);

    socket->connectToHost("127.0.0.1", 9090);

    bool ret = socket->waitForConnected();
    if(ret == false)
    {
        QMessageBox::critical(this, "连接服务器出错", socket->errorString());
        exit(1);
    }
}

Widget::~Widget()
{
    delete ui;
}


void Widget::on_pushButton_clicked()
{
    
}
  1. 接下来在Widget的.cpp源文件中,在Widget的构造函数中,为了区分客户端和服务器程序,我们先使用setWindowTitle设置窗口标题为客户端,接下来给QTcpSocket*类型的指针socket创建实例,所以此时我们new一个QTcpSocket,指定this指针对应的Widget窗口为父元素,挂接到对象树上,让socket对象的生命周期随窗口,当窗口关闭,释放,销毁的时候借助对象树机制自动调用delete析构socket对象,此时我们也就不用在Widget的析构函数中手动delete这个socket对象了
    在这里插入图片描述
  2. 接下来我们就可以调用connectToHost让客户端连接服务器,对于如上参数,虽然多,但是我们只需要关注前两个参数即可,对于第一个参数让我们传入要连接服务器的IP地址,这里我们由于是在同一台主机上启动服务器和客户端,所以这里对于服务器的IP地址就是本地换回地址也就是"127.0.0.1",接下来对于第二个参数我们传入服务器绑定的端口号9090即可
  3. 那么我们调用connectToHost之后,此时内核层面就会和对方服务器进行三次握手流程了,可是这毕竟是网络通信,涉及到网络就有可能会涉及到网络卡了,也就是网络状况不好的情况,即网络传输是会消耗一些时间的,也就是说三次握手是会消耗一定的时间的,而一旦消耗时间,如果是connectToHost是阻塞式等待连接成功,那么此时与其阻塞在这里等待连接成功,那么倒不如去做一些其它的事情
  4. 所以Qt将connectToHost设计成了非阻塞的函数,也就是说connectToHost不会阻塞等待三次握手完毕,而仅仅是发起一个连接请求,然后就直接返回了,接下来就是内核层面和对方服务器进行三次握手流程,这里我们可以对比一下原生的Linux中的api,也就是有一个connect函数,Linux中的socket默认都是阻塞式IO的,所以针对Linux中的socket进行connect也就是阻塞式连接直到连接成功或者超时连接失败才会返回
    在这里插入图片描述
  5. 而由于connectToHost是非阻塞的函数,所以我们还需要使用额外的函数waitForConnected进行等待连接建立的结果,确认客户端连接服务器的结果是否成功,所以此时我们调用waitForConnected进行等待,如上默认阻塞等待的时间是30000毫秒,而1秒等于1000毫秒,也就是默认阻塞等待的时间是30秒,此时如果30秒之内等待客户端连接服务器成功了,那么waitForConnected就会返回true表示连接成功,如果30秒之后,客户端还没有连接上服务器,此时就超时了,那么waitForConnected就会超时返回不再阻塞,此时waitForConnected就会返回false表示连接失败
  6. 所以此时我们使用bool类型的ret接收waitForConnected的返回值,如果ret为false表示连接失败,此时是一个非常严重的问题,客户端连接服务器失败了,那么也就意味着客户端本次无法获取服务器的服务了,那么客户端也就无法让用户进行操作了,所以对于客户端连接服务器出错是非常严重的问题,所以我们使用静态方法QMessageBox::critical弹出严重问题的消息对话框
  7. 接下来对于第一个参数传入this指针,指定父元素为Widget窗口,对于第二个参数传入消息对话框的标题为连接服务器出错,对于第三个参数需要传入消息对话框显示的文本,这里我们传入错误信息,如何获取错误信息呢?出错了就会有errno错误码,接下来我们这里就调用errnoString将errno对应的错误信息转换成字符串的形式进行传参,那么在之后,由于此时客户端连接服务器失败,也就意味着此时客户端启动失败了,即此时客户端无法让用户继续操作了,所以后面的逻辑我们也不用执行了,直接exit终止程序对应的进程,然后返回错误码为1即可
#include "widget.h"
#include "ui_widget.h"
#include <QHostAddress>
#include <QMessageBox>


Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->setWindowTitle("客户端");

    socket = new QTcpSocket(this);

    socket->connectToHost("127.0.0.1", 9090);

    connect(socket, &QTcpSocket::readyRead, this, [=](){
        QString response = socket->readAll();

        ui->listWidget->addItem("服务器说: " + response);
    });

    bool ret = socket->waitForConnected();
    if(ret == false)
    {
        QMessageBox::critical(this, "连接服务器出错", socket->errorString());
        exit(1);
    }
}

Widget::~Widget()
{
    delete ui;
}


void Widget::on_pushButton_clicked()
{
    const QString& text = ui->lineEdit->text();

    socket->write(text.toUtf8());

    ui->listWidget->addItem("客户端说: " + text);

    ui->lineEdit->setText("");
}

在这里插入图片描述

  1. 接下来我们就该编写发送按钮的clicked信号对应的槽函数了,此时用户点击发送按钮,意味着用户已经在单行输入框中输入了内容,所以此时我们使用text将单行输入框中的文本获取到,使用QString类型的text进行接收作为客户端的请求数据,接下来使用write将请求数据发送即可,但是这里不能直接将QString类型的text进行传参,由于TCP是面向字节流的,所以write的参数是字节数组QByteArray,这里我们使用toUtf8将QString类型的text转换为字节数组QByteArray进行传参即可
  2. 接下来我们将客户端说: 加上请求数据拼接成一个字符串,然后使用addItem将拼接好的字符串作为一个元素添加到列表上,也就是将客户端的请求数据回显到界面的列表上,此时用户的请求数据已经发送给服务器了,那么也就意味着单行输入框的内容已经发送出去了,为了便于用户进行下一次的输入,所以我们使用setText将单行输入框的内容置为空串即可
  3. 而服务器在收到客户端的请求数据之后,就要进行业务逻辑的处理,构建响应数据并且发送回给客户端,所以此时我们当前的客户端程序中有进行对于响应数据的读取并回显的逻辑吗?没有,所以接下来我们就要编写在客户端程序中对于来自服务器的响应数据的读取并回显,我们知道,当QTcpSocket类型的对象socket收到来自服务器的响应数据的时候,就会触发readyRead信号,此时我们就可以给这个readyRead信号绑定槽函数去实现这里的读取服务器的响应数据的并回显的逻辑
  4. 所以此时我们就要使用connect连接信号槽了,那么我们将connect的代码放到哪里呢?这里小编建议放到connectToHost之后,waitForConnected之前,因为connectToHost发起连接服务器之后由于是非阻塞所以就会立即返回,由内核完成和对方服务器进行三次握手的流程,而三次握手是要消耗一定的时间的,而我们可以使用waitForConnected等待获取连接的结果,所以我们就可以在三次握手消耗一定的时间的时候执行其它的代码逻辑,也就是这里的去进行connect连接信号槽,这也是connectToHost设计成非阻塞的主要原因
  5. 所以在connectToHost之后,waitForConnected之前,我们使用connect连接socket的readyRead信号和this指针对应的Widget中的槽函数即可,对于这里的槽函数我们简单点,将槽函数设置成lambda表达式即可,以传值捕获的lambda,自然会将Widget的QTcpSocket*类型的私有成员变量socket对象进行捕获,所以此时在lambda表达式中,就可以进行读取服务器的请求数据并回显的操作了
  6. 接下来我们使用readAll将底层TCP的接收缓冲区的数据全部都读取上来,也就是读取服务器发来的响应数据,readAll的返回值是QByteArray类型的,这里由于QString可以使用QbyteArray进行构造,所以我们使用QString类型的response对象接收readAll的返回值即可,接下来我们将服务器说: 加上响应数据拼接成一个字符串,然后使用addItem将拼接好的字符串作为一个元素添加到列表上,也就是将服务器发来的响应数据回显到界面的列表上

二、测试

单个客户端连接服务器

  1. 所以如上,我们就编写完成了TCP回显客户端了,而在之前的文章中小编还实现了TCP回显服务器 详情请点击<——,所以此时我们就可以一并来进行测试 关于如何在Qt Creator中运行多个程序,在第一点UDP回显客户端的实现中进行的讲解,详情请点击<——
  2. 对于服务器和客户端的测试,基本原则一定是先启动服务器,然后再启动客户端,因为只有服务器跑起来了之后,服务器才可以提供服务,客户端的请求才有响应,所以此时我们右击服务器项目,然后点击运行,此时服务器就运行起来了,接下来由于客户端项目的字体比服务器黑,所以代表此时客户端是当前的活动项目,那么我们点击屏幕左下角的运行,此时客户端就会运行了

运行结果如下
在这里插入图片描述

  1. 所以此时服务器先运行起来,客户端后运行,此时我们在客户端的单行输入框中输入aaaaa,然后点击发送,单行输入框的内容就被清空了,此时客户端的列表上就显示了客户端的请求信息aaaaa
  2. 此时服务器就可以收到来自客户端的请求数据aaaaa,接下来服务器基于请求报文中的请求信息构建响应报文,并且将响应报文发送回给客户端,服务器将客户端的IP地址端口号以及请求数据aaaaa,还有服务器的响应数据拼接成字符串QString作为一个元素回显到列表上
  3. 客户端收到了来自服务器的响应数据之后,拿到响应报文中的响应数据aaaaa,将其作为一个元素回显到列表上,同样的小编在客户端的单行输入框输入bbbbb也是同样的原理,这里小编就不再过多解释了

多个客户端连接服务器

  1. 之前小编在讲解Linux关于TCP的网络编程的时候,也写了单进程版的TCP回显服务器 详情请点击<——,那个时候我们遇到了一个问题,当多个客户端同时访问的时候,只有一个客户端可以成功连接服务器并访问,后来我们给TCP回显服务器引入了多线程 详情请点击<——,给每一个客户端安排一个单独的线程,问题才得到改善
  2. 那么我们在Linux中编写的单进程版的TCP回显服务器之所以会出现上述的问题,其实和TCP以及多线程都没有任何关系,并且也从来没有说法,说TCP服务器必须要使用多线程来进行编写,此时我们先来定位一下代码以及对应的讲解在哪里 在第一点TCP服务器TcpServer.hpp(版本一:单进程/单线程版)中的StartServer和Service中进行的讲解,详情请点击<——
  3. 所以对于单进程服务器同时连接多个客户端,只有一个客户端可以成功连接服务器并访问,那么单进程服务器存在这个问题的本质是由于,我们写了双重循环,最外层的for循环用于accept获取连接,里层的while循环用于read读取数据进行回显,那么当第一个客户端连接好服务器之后,别忘了由于服务器是单进程,也就是单执行流,所以此时服务器就会卡在里层循环中,然后while死循环的read读取数据进行回显
  4. 由于此时第一个客户端没有断开连接,所以就不会出现read的写端关闭,也就自然不会read的返回值为0,所以此时里层循环也就不会及时结束,进而导致外层的循环不能调用到accept获取后续客户端的连接,所以此时后续的客户端都无法进行连接服务器并访问,那么当小编将第一个客户端终止之后,立马第二个客户端的连接就可以被服务器获取上来,并且第二个客户端的请求数据也被回显,进行了正常的访问
  5. 关于这里的现象演示在第一点的多进程版中逻辑一的测试中进行的演示,详情请点击<——,虽然这里的现象演示并不是使用单进程服务器进行演示的,是使用的服务器去for创建子进程进行read读取请求数据的形式演示的,但是由于服务器要进行waitpid等待子进程,所以也会卡住,卡住的位置是位于内层while循环的外面,也就是外层for循环中,accept的前面,那么此时小编使用第二个客户端进行连接服务器,所以服务器也照样执行不到accept,第二个客户端无法连接服务器并进行访问,只有当小编终止第一个客户端之后,子进程结束,服务器这个父进程waitpid等待子进程成功,然后才会执行accept获取到第二个客户端的连接,此时第二个客户端才可以成功访问服务器,所以现象一致,这里小编就不再把代码扒出来重新演示现象了
  6. 对于后面引入多线程,本质上就是把双重循环,也就是用于accept获取连接外层循环,和用于read读取请求的内层循环,化简为两个独立的循环,让主线程只负责accept获取连接,把获取上来的连接让新线程read读取请求,这样多线程的TCP服务器就可以同时处理多个TCP客户端的连接以及请求了
  7. 而我们在Qt中写的单进程的TCP回显服务器,那么是否可以同时处理多个客户端的请求呢?下面我么来验证一下,关于如何在Qt程序中同时运行多个客户端程序,在第二点服务器和客户端的测试中的第三点进行的讲解,详情请点击<——,所以下面小编就直接在bulid临时文件中启动运行多个客户端程序进行测试了

运行结果如下
在这里插入图片描述

  1. 此时我们双击.exe可执行程序,那么此时就启动了一个新的客户端程序,并且小编使用这个新的客户端发送一个消息11111,那么新的客户端回显了请求与响应信息,服务器也回显了对应客户端的IP地址,端口号,请求与响应信息,没有问题
  2. 再双击.exe启动一个可执行程序,那么此时就又启动了一个新的客户端程序,并且小编使用这个新的客户端发送一个消息22222,那么新的客户端回显了请求与响应信息,服务器也回显了对应客户端的IP地址,端口号,请求与响应信息,没有问题,
  3. 那么此时我们原来运行的那一个客户端能否运行呢?所以小编使用原有的客户端发送一个消息33333,那么原有的客户端回显了请求与响应信息,服务器也回显了对应客户端的IP地址,端口号,请求与响应信息,没有问题
  4. 下面小编再逐个关闭客户端,此时服务器上成功的显示了客户端下线的消息

在这里插入图片描述

  1. 如上,通过这里的服务器上打印了关于客户端不同的端口号port,此时就可以区分出这三个客户端是不同的客户端,也就意味着此时我们启动了多个客户端成功,服务器可以同时处理多个客户端的请求
  1. 此时在咱们的Qt服务器程序中,没有使用多线程,仍然的使用的单进程,并且一个循环都没写,是通过Qt内置的信号槽来进行驱动的,通过信号槽机制很好的简化了咱们的程序,同样的我们也要意识到,一个正经的TCP服务器,是不太可能会用Qt来写的,因为服务器一般都是不需要图形化界面的,小编在这里编写图形化的TCP服务器,主要是为了带领大家认识学习Qt中关于TCP的类和方法

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐