【Qt】系统相关(九)TCP回显服务器的实现,QTcpServer和QTcpSocket的讲解
一、TCP对应QTcpServer和QTcpSocket的接口讲解二、TCP回显服务器的实现
·
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
Qt系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
前言
【Qt】系统相关(八)UDP回显客户端的实现,测试,问题思考——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【Qt】系统相关(九)TCP回显服务器的实现,QTcpServer和QTcpSocket的讲解
一、TCP对应QTcpServer和QTcpSocket的接口讲解
- 通过之前在Linux网络编程中的学习 关于Linux网络编程的讲解,详情请点击<——,我们知道了UDP和TCP的主要特性
(1)UDP是无连接的,不可靠传输,面向数据报,全双工
(2)TCP是有连接的,可靠传输,面向字节流,全双工 - TCP的可靠传输,也就意味着TCP要比UDP做出更多努力,这些系统内核的传输层已经帮我们实现了,那么在内核层面通过三次握手,四次挥手,校验和,确认应答,超时重传,快重传,滑动窗口,流量控制和拥塞控制等机制保障的,这些小编在Linux网络编程中已经进行了详尽的讲解,这里小编就不再展开讲解了
- 而TCP的有连接的,面向字节流的则更靠近应用层一点,和我们程序员去使用TCP息息相关,也就意味着TCP的有连接的,面向字节流的同样要比UDP做出更多的努力,更多的努力也就代表实现上相对复杂一点,而对应提供给我们的接口也就相对多一点,所以TCP的代码要比UDP的代码要复杂一点
- TCP和UDP一样,都是被直接写入到了系统内核,Qt中的TCP是对系统提供的API进行的封装,Qt中同样对TCP封装成了类,与之相关的有两个类,分别是QTcpServer和QTcpSocket
(1)QTcpServer用于进行TCP的三次握手,也就是负责TCP连接建立的过程,进行绑定IP和端口,获取客户端的连接
(2)QTcpSocket用于客户端和服务器之间基于字节流的数据交互,用于客户端向服务器发起连接请求 - 对于三次握手这部分真正的功能实现是由操作系统内核实现的,Qt只是对系统提供的API进行的封装,我们编写的应用层的代码也只是用于告诉内核,让内核帮我们完成三次握手的相关流程,对于客户端来讲,我们编写代码就是告诉内核,我要向服务器发起连接请求,对于服务器来讲,我们编写代码就是告诉内核,我要将一个已经和客户端建立好的连接获取上来
- QTcpServer是专门给服务器使用的类,下面我们来认识一下QTcpServer类的相关接口
(1)listen(const QHostAddress&, uqint16 port),用于绑定指定的IP地址和端口号,并且开始监听
……(1)Qt中的这一个listen函数其实内部封装了Linux的两个系统调用分别是bind和listen
……(2)系统的bind用于绑定执行的IP地址和端口号
……(3)系统的listen用于开始监听连接的到来
(2)nextPendingConnection(),可以从系统中获取到一个已经和客户端建立好的TCP连接,返回一个QTcpSocket,表示和客户端的连接,用这个QTcpSocket对象和客户端进行通信
(3)newConnection,这个是一个信号,当和客户端建立好连接之后就会触发该信号,类似于IO多路复用的通知机制,具体点来讲也就是类似于通过在网卡驱动程序中注册回调函数,当和客户端建立好连接之后就将连接放到就绪队列中,通知上层获取连接 - QTcpSocket用于客户端和服务器之间的数据交互,对应的接口如下
(1)readAll(),读取当前TCP接收缓冲区的所有数据,返回QByteArray对象
(2)write(const QByteArray&),发送数据,本质上是将数据写入到TCP的socket发送缓冲区中,什么时候发,发多少,出错了怎么办由操作系统负责
(3)deleteLater(),暂时把socket对象标记为无效,Qt会在下个事件循环中析构释放该对象,类似于半自动垃圾回收
……(1)对于事件循环,其实和信号槽和事件机制都有关系,可以简单理解为Qt程序内部,带有一个生物钟这样的东西,会周期性的执行一些逻辑
……(2)在周期性执行一些逻辑的过程中,可以引入事件机制,也就是当对应的事件触发了,那么执行一些逻辑就多做一些事情,也就是调用对应事件的事件处理函数,如果没有触发对应的事件,那么这次执行一些逻辑就少做一些事情
……(3)而Qt中信号槽的底层就是事件机制,可以认为事件也可以理解成信号,事件的处理函数也可以理解成槽函数
(4)readyRead,这是一个信号,当TCP的接收缓冲区接收到数据之后就会触发该信号,类似于IO多路复用的通知机制
(5)disconnected,这是一个信号,当客户端和服务器的连接断开的时候就会触发该信号,类似于IO多路复用的通知机制
二、TCP回显服务器的实现

- 那么在学习了TCP的相关类以及接口之后,下面我们就可以来实现一下TCP回显服务器了,而TCP回显服务器的布局和UDP回显服务器的布局一致 关于UDP回显服务器的讲解,详情请点击<——,有了UDP回显服务器的讲解,这里关于TCP回显服务器的实现就容易许多,所以对于TCP回显服务器的界面大致如上,界面上放一个QListWidget列表控件,用于显示客户端的IP地址,端口号port,客户端的请求信息,服务器的响应信息

- 所以接下来我们创建一个项目名为,基类为QWidget,派生类为Widget的项目,由于我们要使用TCP进行网络编程,所以要使用Qt中的网络模块,那么如上,我们在项目的.pro文件的第一行加入network模块用于引入Qt的网络模块,然后左下角运行项目让Qt解析.pro文件,进而能让我们正常包含TCP相关的头文件,如上操作的至于具体的原因小编在UDP回显服务器中已经进行了详细讲解了,这里小编就不再赘述了,并且TCP回显服务器中很多的内容,在UDP回显服务器中也进行了讲解,关于UDP回显服务器中的内容小编不会在TCP回显服务器中进行二次讲解,这里也提醒大家一定要学完UDP回显服务器的实现之后再来学习TCP回显服务器的实现

- 接下来我们点击ui文件,进入Qt Designer,所以此时我们拖拽左侧红框内的列表控件,然后调整成上图界面即可,objectName保持不变
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QTcpServer* tcpServer;
};
#endif // WIDGET_H
- 在Widget的.h头文件中,由于我们要进行TCP服务器的创建,绑定,监听,获取连接等操作,所以此时我们声明一个私有成员变量QTcpServer*指针类型tcpServer
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
}
- 所以接下来我们在Widget的.cpp文件中,在Widget的构造函数中,为了区分服务器和客户端,我们使用setWindowTitle设置Widget窗口标题为服务器,接下来我们就给tcpServer创建实例,此时我们new一个QTcpServer对象,指定父元素为Widget窗口对应的this指针,挂接到对象树上即可,此时当Widget窗口被关闭,销毁的时候,就会自动调用delete析构tcpServer对象,接下来我们是否可以直接调用listen绑定端口号并监听呢?
- 不行,如果我们调用listen绑定端口号并监听,那么也就意味着此时客户端可以连接服务器了,但是此时我们的代码逻辑中并没有将连接获取上来,并且处理请求的逻辑呀,所以此时当客户端连接成功服务器之后,但是由于我们并没有将连接获取上来,同样的对于客户端的任何请求我们也没有对应的处理请求逻辑的实现,所以客户端界面上明明显示的服务器连接成功,但是用户此时在客户端上进行任何的操作,发送任何的请求,服务器都不会进行响应,此时就有问题了
- 所以我们不应该先调用listen绑定端口号并监听,而是要先进行连接的获取,并且在连接的获取对应的槽函数中进行请求逻辑的处理,当我们可以顺利的把连接获取上来,并且可以处理客户端的请求了之后,此时再先调用listen绑定端口号并监听即可,所以这里一定是先把如何处理连接,如何处理请求等准备好,然后再listen绑定端口号并监听给客户端提供服务,并且对于listen绑定端口号并监听操作一定要是初始化的最后一步
- 对于上述步骤其实也很符合我们的刻板印象,例如开一家餐馆,要先准备好店面,厨师,店员,食材等,然后才开张营业,如果要是先开张营业,而不去准备厨师,食材等,那么客户来了之后,老板,我要吃麻辣小龙虾,而你作为老板解释说,不好意思,我们还没有厨师,无法上菜,这合理吗?这不合理,这不科学,所以一定是先准备好店面,厨师,店员,食材等,然后才开张营业
- 还没有没调用listen绑定端口号并监听之前,此时正在进行准备如何处理连接,如何处理请求等,如果客户端连接服务器会是什么样子呢?此时客户端上会显示连接服务器失败,请刷新重试,也就是此时还没有连接服务器成功,所以自然的用户就会认为,此时服务器还没有办法提供服务,只有当服务器连接成功之后才可以提供服务,同样的道理,此时如果还在准备店面,厨师,店员,食材等,客户来了一看,原来还没有开张营业,那么客户就会等到开张营业了之后再来消费
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpServer>
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 processConnection();
private:
Ui::Widget *ui;
QTcpServer* tcpServer;
};
#endif // WIDGET_H
- 所以关于如何处理到来的连接,我们同样可以使用信号槽机制,服务器和客户端建立好连接之后,QTcpServer就会触发newConnection信号,此时我们就可以给这个信号绑定槽函数,然后再槽函数中把连接获取上来,进行处理请求等操作即可,在Widget的.h头文件中,我们声明一个私有成员的槽函数processConnection
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
}
- 接下来我们就可以在Widget的.cpp源文件中定义这个槽函数processConnection,对于如何实现槽函数processConnection,小编这里先个搁置一下,等到完成了Widget的构造函数的编写之后再来实现,所以在Widget的构造函数中,我们使用connect去进行tcpServer的newConnection信号和this指针对应的Widget中槽函数processConnection的连接
- 此时在槽函数processConnection中已经准备好了如何将连接获取上来,如何处理请求之后,接下来我们才可以进行listen绑定端口号并监听,那么此时我们调用listen,传入IP地址为QHostAddress::Any表示接收主机上的网卡上的全部IP地址的请求,然后绑定端口号为9090即可,此时listen函数就会自动进行监听端口号了,那么此时问题来了,是否listen会由于权限不足,端口号已经被其它人绑定了等原因造成绑定端口号失败呢?

- 完全有可能,如上,恰好listen的返回值就是bool类型的值,所以此时我们使用ret接收一下listen的返回值,进行判断即可,如果是true则表示listen绑定端口号成功并且进行了监听,此时我们不用进行差错处理,而如果ret的值是false,表示此时listen绑定端口号失败了,那么就代表此时权限不足或者端口号port被占用等其它的因素,那么此时我们就使用QMessageBox::critical弹出一个严重问题的消息对话框
- 接下来传参父元素为Widget窗口对应的this指针,挂接到对象树上,然后继续传参严重问题的消息对话框的标题为服务器启动失败,接下来传参严重问题的消息对话框中显示的文本,此时我们知道bind绑定失败的原因吗?好像不太清楚,对于此时bind绑定失败的原因有可能是端口号port被占用或者IP地址错误或者权限不足或者端口号非法等原因
- 所以对于bind绑定失败的原因,我们确实不太清楚,那么谁清楚呢?系统清楚,系统调用bind失败时候,会设置错误码errno,而在C语言中使用perror就可以将errno对应的错误信息以字符串的形式进行打印,恰好Qt中对于errno机制同样封装成了errorString,这里的Qt中的errnoString的作用就和C语言中的perror类似,所以我们这里就调用errnoString将errno对应的错误信息转换成字符串的形式进行传参,那么在弹出严重问题的消息对话框之后,由于此时服务器绑定端口失败,也就意味着此时服务器启动失败了,即此时服务器无法为客户端提供服务了,所以后面的逻辑我们也不用执行了,直接exit终止程序对应的进程,然后返回错误码为1即可
- 这里我们准备好了如何将连接获取上来,如何处理请求之后,才进行的listen绑定端口号并监听,那么当执行完成listen之后也就意味着此时客户端可以连接服务器,发送请求报文进行请求服务了,可是这里我们作为服务器知道客户端什么时候来连接吗?不知道,服务器永远都是被动的,服务器给客户端提供服务,当客户端有需求的时候,客户端才会连接服务器,发送请求报文进行请求服务,此时服务器才可以进行连接的获取,请求的处理
- 所以此时即使我们准备好了如何将连接获取上来,如何处理请求之后,并且进行了listen绑定端口号并监听,作为服务器端也并不清楚客户端什么时候来连接,所以对于newConnection信号什么时候触发我们也不清楚,进而槽函数processConnection什么时候被执行我们也不清楚,只有当客户端有需求的时候才会连接服务器,发送请求报文进行请求服务器的服务的时候,才会触发newConnection信号,进而执行槽函数processConnection将连接获取上来,并且获取请求进行处理
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QTcpSocket>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
// 获取连接
QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线";
ui->listWidget->addItem(log);
// 处理请求
connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
// 读取请求数据
QString request = clientSocket->readAll();
// 业务逻辑的处理
const QString& response = process(request);
// 发送响应数据
clientSocket->write(response.toUtf8());
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "], req: "
+ request + ", resp: " + response;
ui->listWidget->addItem(log);
});
// 处理连接的断开
}
QString Widget::process(const QString &request)
{
return request;
}
- 接下来我们就开始编写连接获取,请求处理对应的槽函数processConnection,首先当槽函数processConnection被执行的时候,就代表触发了newConnection信号,而newConnection信号的触发就代表此时已经和客户端建立了一个连接,所以此时我们使用nextPendingConnection通过tcpSocket拿到一个clientSocket对象,通过这个对象,我们就可以和客户端进行通信,所以这个对象就像服务器和客户端的代理人一样的存在
- 此时拿到了clientSocket对象就代表客户端上线了,接下来我们回显一个日志log,包含客户端的IP地址和端口号port即可,但是如何获悉客户端的IP地址和端口号port呢?其实在我们获取上来的clientSocket对象中就有,我们使用peer系列接口进行获取,peer系列接口可以获取对端的信息,当前我们是服务器端,那么此时对端就是客户端,所以此时站在服务器端调用peer系列接口就是获取的对端客户端的信息,而如果站在客户端那么调用peer系列接口就是获取的对端服务器的信息
(1)我们使用peerAddress获取对端的IP地址,返回值是一个QHostAddress类型,那么我们通过toString装换成QStrring类型进行字符串拼接即可
(2)使用peerPort获取对端的端口号port,返回的是一个quint16类型,那么我们使用QString::number转换为QString类型进行字符串拼接即可 - 所以此时我们构造好了log对象,那么回显到列表上了吗?还没有,当前的log对象还没有作为元素添加到列表上进行回显,所以此时我们使用addItem将log对象作为一个元素回显到列表上即可,此时我们成功的获取了一个客户端连接,并且回显了客户端上线的日志,而客户端上线了,此时有可能客户端会进行操作给我们发来请求报文,也有可能客户端不会立即进行操作给我们发来请求报文,但是不论如何,当客户端给我们发来请求的时候,我们作为服务器应该具有能力去处理客户端的请求报文
- 所以此时我们就可以借助信号槽机制来实现处理客户端的请求报文,当服务器收到客户端的请求报文的时候,会触发readyRead信号,此时我们就可以给readyRead信号绑定槽函数,在槽函数中进行请求报文的获取,业务逻辑的处理,响应报文的构建与发送,这里由于要使用到clientSocket读取请求报文,发送响应报文和客户端进行通信,而这个clientSocket是一个QTcpSocket*指针类型,在槽函数内需要使用
- 那么是否我们可以将这个clientSocket是一个QTcpSocket*指针类型作为参数传参给槽函数呢?其实是不行的,槽函数最终的传参,不是我们来进行传参,而是基于事件循环Qt来进行调用,对于槽函数的传参逻辑我们无法干预,而我们之前学习过槽函数也可以是lambda表达式,lambda可以以传值的方式捕获这个clientSocket是一个QTcpSocket*指针类型,正好符合我们的需求

- 所以此时我们使用connect将clientSocket发出的readyRead信号和this指针对应的Widget中的lambda表达式形式的槽函数进行连接,那么在lambda表达式中就可以进行请求报文的获取,业务逻辑的处理,响应报文的构建与发送的逻辑了,此时我们可以使用readAll将本次请求报文读取上来,那么这里耐心的读者友友可能会发现,与UDP中使用的接口不同,UDP读取报文的时候使用的是receiveDatagram读取一个请求报文,返回的是UDP数据报QNetworkDatagram对象,代表UDP是面向数据报的
- 而我们这里使用readAll一次性将TCP接收缓冲区的数据全部读取上来,返回值却是一个QByteArray字节数组,也就是一个字节一个字节的数据,合理吗?十分合理,一个一个字节,这就是TCP面向字节流的体现,后面小编会进行详细讲解,这里我们先知道是TCP面向字节流的体现即可,readAll返回的是QByteArray字节数组,我们这里使用QString接收即可,因为QString对象可以使用QByteArray构造转换,由于TCP是面向字节流的,所以这里叫请求报文不合理,叫请求数据比较合理
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
QString process(const QString& request);
private slots:
void processConnection();
private:
Ui::Widget *ui;
QTcpServer* tcpServer;
};
#endif // WIDGET_H
- 此时我们已经将请求数据拿到了,接下来就要基于请求数据构建响应数据了,而将请求数据计算转换成响应数据也是整个TCP服务器最为核心的模块,所以这里我们在Widget的.h头文件中声明一个私有的业务处理函数process,参数为const QString& request用于接收请求数据,返回值是QString用于返回响应数据
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QTcpSocket>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
// 获取连接
QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线";
ui->listWidget->addItem(log);
// 处理请求
connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
// 读取请求数据
QString request = clientSocket->readAll();
// 业务逻辑的处理
const QString& response = process(request);
// 发送响应数据
clientSocket->write(response.toUtf8());
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "], req: "
+ request + ", resp: " + response;
ui->listWidget->addItem(log);
});
// 处理连接的断开
}
QString Widget::process(const QString &request)
{
return request;
}
- 而由于我们编写的是TCP回显服务器,所以这里的业务逻辑中响应数据也就是请求数据,所以这里我们直接将请求数据作为响应数据返回即可,同样的,这里我们也要认识到由于我们这里编写的是TCP回显服务器,所以业务逻辑很简单,一行代码就搞定了,但是如果是一个商业服务器,那么这里的业务逻辑将会很复杂,很复杂,可能会需要好几万行代码甚至更多,才可以搞定
- 接下来我们返回lambda表达式中,将请求数据request传入调用业务处理函数process即可,使用const QString&类型的对象接收process函数的返回的临时的响应数据,这里使用const QString&接收函数返回的临时变量是可以延长临时变量的生命周期,接下来有了响应数据,我们就该将响应数据发送给客户端,那么这里使用write即可发送

- 如上,我们注意到write的输入性参数也是一个QByteArray字节数组,毫无疑问,这很合理,一个一个字节的进行返回响应数据,符合TCP面向字节流的概念,这里我们对比一下UDP中使用的是writeDatagram,发送的是一个QNetworkDatagram数据报对象,同样很合理,符合UDP面向数据报的概念,所以从这里我们可以看出,其实Qt对于这些概念的维护是十分好的
- 而在UDP中使用writeDatagram发送一个QNetworkDatagram数据报对象,在QNetworkDatagram数据报对象中包含了发送的客户端的IP地址和端口号port,需要我们在创建QNetworkDatagram的时候手动指定发送的客户端的IP地址和端口号,那么在TCP中使用write发送的是一个QByteArray字节数组对象,又从哪里获取发送的客户端的IP地址和端口号呢?从clientSocket中获取
- 别忘了write是clientSocket调用的,在clientSocket中既然我们都可以通过peerAddress和peerPort获取IP地址和端口号port,所以也就说明了clientSocket中包含有客户端的IP地址和端口号port,所以这里对于TCP发送响应数据的时候,我们不需要显示指定发送的客户端的IP地址和端口号,而是由Qt自动根据clientSocket中包含的客户端的IP地址和端口号进行发送响应数据
- 所以此时服务器已经给客户端发送了响应数据了,而我们要实现的是一个TCP回显服务器,所以此时我们把客户端的IP地址,端口号port,客户端的请求数据,服务器的响应数据进行字符串的拼接,构造成一个log对象,通过addItem将log对象作为一个元素回显到Widget界面上的列表中即可
// 处理请求
connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
// 读取请求数据
QString request = clientSocket->readAll();
// 业务逻辑的处理
const QString& response = process(request);
// 发送响应数据
clientSocket->write(response.toUtf8());
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "], req: "
+ request + ", resp: " + response;
ui->listWidget->addItem(log);
});
- 那么此时小编将请求的处理单独拎出来,对比一下Qt中TCP网络编程和Linux中TCP网络编程的写法,在Linux的TCP网络编程中,我们需要搞一个循环,循环的读取请求,循环的进行处理,而在Qt的TCP网络编程中,我们就不必采用循环的写法了,而是通过信号槽机制,每次客户端发来请求数据之后,在Qt中都可以通过QTcpSocket触发readyRead信号,进而就会执行关联的槽函数
- 并且在Qt中,如果涉及到客户端发送多个请求数据,那么同样的,在Qt中也可以通过QTcpSocket触发多次readyRead信号,进而就会多次的执行关联的槽函数,所以循环读取的方式也就在Qt中被信号槽机制替代了,而Qt中这种信号槽机制,以及对于系统原生API的封装可以大大的降低我们编写代码的难度,简化网络编程代码的编写,让我们更加专注业务逻辑的编写
- 其实上述代码不够严谨,但是作为TCP回显服务器是足够了的,在实际使用TCP的过程中,由于TCP是面向字节流的,那么一个完整的请求,可能会被分成多段的字节数组进行传输,而我们知道,对于完整的请求,也包括响应,这些都是应用层的协议,而TCP处于传输层的协议,虽然TCP已经帮我们处理的很多棘手的问题了,可以确保请求和响应数据可以安全有序的传输到对端
- 但是由于TCP协议位于传输层,传输层的TCP协议只认识并解析传输层的TCP协议报文格式,而应用层的协议才能认识并解析应用层的协议,所以TCP并不负责区分,对于多段字节数组中的数据,从哪里到哪里是一个完成的应用层数据报,而对于区分多段字节数组中的数据,从哪里到哪里是一个完成的应用层数据报这本质上就涉及到了粘包问题的处理了
- 更严谨的做法应该是将每次收到的数据都放到一个大的字节数组缓冲区中,并且通信双方提前约定好应用层协议的格式,也就是如何处理粘包问题,约定好究竟是按照分隔符来划分完成的应用层数据报,还是采用固定长度大小,也就是定长报文的方式来划分应用层数据报,还是采用定长报头+描述有效载荷大小的字段来划分应用层数据报,亦或者是其它的方式来划分应用层数据报
- 此时约定好了应用层协议的格式之后,就可以按照应用层协议格式对大的字节数组缓冲区中的数据进行更细致的解析处理了,那么我们在这里由于是TCP回显服务器,只需要将获取到的请求和响应数据回显到列表中即可,而请求和响应的数据一般很少,TCP一般也不会将本就很少的请求和响应的数据分多次发送,所以这里我们就不写这么复杂了
- 那么问题来了,客户端连接好服务器之后,客户端给服务器发送请求数据,客户端收到服务器的响应数据,经过几轮之后,如果此时客户端已经使用完服务器提供服务了,客户端没有请求要发送给服务器了,那么我作为用户使用的客户端,不需要使用服务器了,所以我用户就要关掉客户端,而一关掉客户端就意味着客户端要和服务器进行四次挥手断开连接,而我们的服务器的处理逻辑中还没有对于断开连接的处理,这问题大吗?
- 大,非常大,服务器通过nextPendingConnection获取到和客户端进行交流的QTcpSocket*指针类型的clientSocket对象,而服务器是要给客户端提供服务的,服务器只有一个,客户端却有很多个,N个,而对于每一个和服务器进行连接的客户端都要有一个QTcpSocket*指针类型的clientSocket对象,这个clientSocket对象的数目和连接服务器的客户端的数据是相同的,存在很多个,N个
- 那么此时随着服务器的不断运行,服务器服务的客户端会越来越多,也就意味着QTcpSocket*指针类型的clientSocket对象也会越来越多,别忘了QTcpSocket*指针类型的clientSocket对象是要占用堆上的空间的,客户端一般不会一直要求进行服务,一般是服务一段时间之后,客户端就要进行断开和服务器的连接,所以也就意味着此时的QTcpSocket*指针类型的clientSocket对象没有作用了
- 那么如果作为服务器端不对没有作用的QTcpSocket*指针类型的clientSocket对象进行释放,此时累计的QTcpSocket*指针类型的clientSocket对象会越来越多,越来越多,就会产生内存泄露,而QTcpSocket*指针类型的clientSocket对象的底层是会对应一个文件打开对象的,而文件打开对象的指针要被放到进程PCB中指向的文件描述符表的下标位置上,占用一个文件描述符,那么随着clientSocket对象会越来越多,越来越多,占用的文件描述符也会越来越多

- 所以此时就产生了双重的泄露,分别是内存泄露和文件描述符泄露,此时问题就非常严重了,那么究竟哪一个先被泄露完呢?我们来分析一下,首先就是由于内存便宜,所以现在的机器一般都不缺内存,而相对来讲文件描述符表的长度,则是操作系统的一个参数,Linux的ubuntu系统上使用ulimit -n查看当前文件描述符的个数,这个也就是文件描述符表的长度,可以使用ulimit命令进行设置调整,调整的上限值我们可以使用ulimit -Hn进行查看是1048576个,所以相对来讲文件描述符表的长度要更紧张,是更容易泄露完的,即文件描述符泄露相对于内存泄露,文件描述符更紧张,更容易泄露完
- 而在服务器端有一个QTcpServer对象,在客户端有一个QTcpSocket对象,客户端使用这一个QTcpSocket和服务器进行连接,通信,连接断开的操作,那么关于对于服务器端的一个QTcpServer对象,客户端的一个QTcpSocket对象,其实占用的内存很小,也就占用一个文件描述符,其实如果就算不释放服务器端的一个QTcpServer对象,客户端的一个QTcpSocket对象的话,内存资源,文件描述符资源也完全够用
- 关键的是服务器端要给每一个进行连接客户端对象都申请一个QTcpSocket对象,服务器端的多个QTcpSocket对象,如果客户端断开连接了之后,这些多个QTcpSocket如果不进行释放,那么会产生内存泄露,文件描述符泄露,所以我们在服务器端当客户端断开连接之后,也就意味着在服务器端和这个客户端连接对应的QTcpSocket对象不需要使用了,所以服务器端需要将不使用的QTcpSocket对象给释放掉,避免造成内存泄露,文件描述符泄露
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QTcpSocket>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if(ret == false)
{
QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
// 获取连接
QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线";
ui->listWidget->addItem(log);
// 处理请求
connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
// 读取请求数据
QString request = clientSocket->readAll();
// 业务逻辑的处理
const QString& response = process(request);
// 发送响应数据
clientSocket->write(response.toUtf8());
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "], req: "
+ request + ", resp: " + response;
ui->listWidget->addItem(log);
});
// 处理连接的断开
connect(clientSocket, &QTcpSocket::disconnected, this, [=](){
// delete clientSocket;
clientSocket->deleteLater();
QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端下线";
ui->listWidget->addItem(log);
});
}
QString Widget::process(const QString &request)
{
return request;
}
- 而当客户端断开和服务器的连接的时候,也就是要进行TCP四次挥手的过程,此时在服务器端的QTcpSocket就会触发disconnected信号,那么此时我们使用connect连接信号槽,而对于槽函数中也需要使用到QTcpSocket类型的对象clientSocket,所以对于这里的槽函数我们同样使用lambda表达式的形式定义,那么我们使用connect将clientSocket发出的disconnected信号和this指针对应的Widget中的lambda表达式进行连接,此时触发了disconnected信号之后,就会执行槽函数对应的lambda表达式中服务器处理和客户端断开连接的逻辑
- 对于lambda表达式我们使用传值的方式捕获变量,那么由于此时客户端已经要和服务器断开连接了,所以也就意味着此时的clientSocket对象我们要释放掉,那么在槽函数对应的lambda表达式中,我们上来直接使用delete释放clientSocket是否可以呢?可以,但是不推荐,虽然调用delete释放clientSocket之后,会自动调用clientSocket的析构函数close关闭对应的文件描述符,然后释放clientSocket对象在堆上占用的空间,此时也就不存在文件描述符泄露和内存泄露了,看似很完美,实则危机四伏,为什么呢?如下
- 当前槽函数对应的lambda表达式,主要是围绕着clientSocket进行操作的,那么一旦delete之后就意味着其它的逻辑中,处于即delete clientSocket后面的代码无法使用clientSocket,此时就很麻烦,所以可以使用delete clientSocket,但是有两个硬性要求,如下
(1)务必要保证delete是这个槽函数中的最后一步
(2)务必要保证delete在槽函数可以执行的到,不会由于return,抛出异常等原因跳过 - 而对于使用delete clientSocket的两个硬性要求,其实在编码上也都可以做得到,那么对于第一个硬性规定来讲,需要程序员人为在最后一步使用delete,对于第二个硬性规定来讲,在槽函数内部使用try catch捕获异常和return,然后在捕获异常和return的位置进行判断是否delete,如果没有delete,那么则进行delete即可,小编,小编,这样有点麻烦了吧,是的,确实麻烦了点,有没有什么简单点的方式呢?有的,有的
- 有的,有的,Qt中为我们提供了一个半自动垃圾回收器,那么就是用QTcpSocket类型的clientSocket对象调用deleteLater即可,对于这个操作来讲,并不是立即销毁clientSocket,而是告诉Qt,在下一轮事件循环中,再进行上述的销毁clientSocket的操作,而我们应该如何理解这里的下一轮事件循环呢?
- 根据小编之前对于事件循环的讲解,我们知道槽函数都是在事件循环中执行的,进入下一轮事件循环,也就意味着上一轮事件循环肯定结束了,也就意味着当前的槽函数肯定是执行结束了,所以在当前槽函数执行结束之后再来执行销毁clientSocket的操作,所以此时只要在槽函数中调用了deleteLater,那么销毁clientSocket的操作肯定可以执行,那么由于是执行完成槽函数之后,再来销毁clientSocket,所以在槽函数中,clientSocket的生命周期始终存在,所以在槽函数中我们可以任意调用clientSocket,此时有的读者友友可能还有疑惑,小编,如果发生了return,抛异常,那么销毁clientSocket的操作被执行吗?
- 会执行,因为一旦return,抛异常了之后,也就意味着此时的槽函数已经结束了,换言之,只要在槽函数中执行了deleteLater,那么在槽函数结束之后,进入下一轮事件循环的中,就会对clientSocket进行销毁释放,此时就会自动调用clientSocket的析构函数close关闭对应的文件描述符,然后再释放clientSocket对象在堆上占用的空间,此时也就不存在文件描述符泄露和内存泄露了,完美
- 当前,其实上述的做法都是权宜之计,相比之下,Java,Python,Go等语言中的全自动垃圾回收机制更好用一些,那么小编,小编,C++也是主流语言呀,为什么C++中不推出全自动垃圾回收器呢?C++是一个极度追求和注重性能的一门语言,全自动垃圾回收机制的引入势必会带来性能的下降,所以C++不会推出全自动垃圾回收器,并且C++标准委员会也明确表示C++不会推出全自动垃圾回收器
- 而Qt作为C++的框架,自然也要全力支持C++中不推出全自动垃圾回收器,虽然Qt不会推出全自动垃圾回收器,但是Qt可以推出半自动垃圾回收器deleteLater呀,Qt主要用来开发图形化的客户端程序,对于客户端来讲对于性能反而是不那么注重,所以Qt在自己的能力范围之内推出半自动垃圾回收期deleteLater也没问题
- 所以此时我们在槽函数中使用QTcpSocket类型的clientSocket调用deleteLater,此时就可以确保clientSocket在当前槽函数结束后,下一轮事件循环中被销毁,释放文件描述符,释放内存,其实这里说释放文件描述符还是有点不准确,更准确来讲是释放进程PCB中指向的文件描述符表中对应文件描述符的数组下标中指针指向的TCP网络相关的文件打开对象,一旦TCP网络相关的文件打开对象被释放之后,自然文件描述符表中对应的数组下标中的指针也要被置空nullptr,自然文件描述符也就被释放了,此时文件描述符资源也就被释放了
- 接下来我们还期望回显日志,所以此时就想要拼接log日志,这里我们简单一点,ctrl+c复制一下当初获取了客户端clientSocket对象那里拼接的log日志,然后ctrl+v粘贴到当前槽函数对应的lambda表达式中,将客户端上线修改为客户端下线即可,俗话说得好,一时复制一时爽,一直复制一直爽呀😝,接下来我们使用addItem将log日志作为元素添加到Widget界面上的列表中即可,此时客户端下线的日志就在Widget界面上的列表中回显了
- 此时我们的TCP回显服务器就实现完成了,由于此时我们没有实现TCP回显客户端,此时我们还没有办法进行测试,需要等下一篇小编实现TCP客户端之后再来进行统一的测试
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)