目录
描述
函数
使用
服务器
准备工作
声明相关函数与对象
绑定并监听
定义槽函数与连接
瑕疵
释放
客户端
准备工作
声明相关函数与对象
初始化并连接服务器
给发送添加槽函数
连接信号槽处理响应函数
测试运行
补充
代码
客户端
服务器
描述
有UDP,自然少不了TCP,不过TCP稍微要复杂一点点
使用 Qt 内部封装好的类去实现一个 TCP 的网络程序
函数
核⼼类是两个: QTcpServer 和 QTcpSocket
QTcpServer ⽤于监听端⼝, 和获取客⼾端连接
名称 | 类型 | 说明 | 对标原⽣ API |
listen(const QHostAddress&, quint16 port) | ⽅法 | 绑定指定的地址和端⼝号,并开始监听. | bind 和 listen |
nextPendingConnection() | ⽅法 | 从系统中获取到⼀个已经建⽴好的 tcp 连接. 返回⼀个 QTcpSocket , 表⽰这个客⼾端的连接. 通过这个 socket 对象完成和客⼾端之间的通信. | accept |
newConnection | 信号 | 有新的客⼾端建⽴连接好之后触发. | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
QTcpSocket ⽤⼾客⼾端和服务器之间的数据交互.
名称 | 类型 | 说明 | 对标原⽣ API |
readAll() | ⽅法 | 读取当前接收缓冲区中的所有数据. 返回 QByteArray 对象. | read |
write(const QByteArray&) | ⽅法 | 把数据写⼊ socket 中. | write |
deleteLater | ⽅法 | 暂时把 socket 对象标记为⽆效. Qt 会在下个事件循环中析构释放该对象. | ⽆ (但是类似于 "半⾃动化的垃圾回收") |
readyRead | 信号 | 有数据到达并准备就绪时触发. | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
disconnected | 信号 | 连接断开时触发. | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
使用
和UDP一样,我们创建一个回显服务器和客户端,界面设置如下
服务器
准备工作
加上 network
界面设置
声明相关函数与对象
头文件包含,并定义好 槽函数和server指针
绑定并监听
定义槽函数与连接
对端的地址和端口是相对的,这一点在客户端也是如此,服务端的对端就是客户端
因为信号槽的机制,所以我们并不需要循环等待
只需要等待信号的触发就可以了
瑕疵
这里的书写是不严谨的,在 TCP 中,由于数据的传输是面向字节流的,所以肯定有字节流相关的问题需要解决,在这里的代码中,我们并没有进行 应用层 上的处理数据,没有定义协议,所以关于数据粘包问题是不能够处理的
释放
和UDP不一样的是,这里我们需要手动释放资源,一个客户端连接对应一个 clientSocket,对应一个文件描述符,而文件描述符则是有限资源,容易因为泄漏问题导致资源紧张,甚至崩溃
关于释放,也要十分的小心,要务必保证释放是槽函数的最后一步进行的,这样才不容易造成内存泄漏和文件描述符泄漏
一方面因为 C++ 中并没有垃圾回收机制,Qt 显然也是没有的,但是 Qt,为了应对这方面的问题,引入了一个 半自动回收的函数,deleteLater,延后回收,这一点就能让资源回收,并且不会影响现有槽函数
客户端
准备工作
界面设置
和 udp 界面设置一样,调整好比例,和拉伸系数即可
声明相关函数与对象
初始化并连接服务器
这里和在 Linux 里有点不一样,Linux里面是阻塞式的连接(简单一些),而 Qt 则是封装成了非阻塞式的连接(速度更快,因为可以处理其他的事件),因为 三次握手 其实是需要消耗一定的时间的
给发送添加槽函数
连接信号槽处理响应函数
测试运行
单个客户端
断开连接
多个客户端
注意要找到 exe 可执行程序,在客户端对应的 build 文件夹中
连接测试运行正常
断开连接测试正常
补充
TCP没有必须要求使用多线程去完成
在很久之前的 Linux 之中的回显服务器的书写是用到了双层循环
当然了一个 TCP 的服务器一般是不会使用 QT 来写的
代码
客户端
Widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QTcpSocket>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;QTcpSocket* socket;
};
#endif // WIDGET_H
Widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1.设置窗口标题this->setWindowTitle("客户端");// 2.创建 socket 对象的实例socket = new QTcpSocket(this);// 3.和服务器建立连接socket->connectToHost("127.0.0.1", 9090);// 4. 连接信号槽,处理响应connect(socket, &QTcpSocket::readyRead, this, [=](){// a)读取出响应数据QString response = socket->readAll();// b)把响应内容显示到界面上ui->listWidget->addItem("服务器说: " + response);});// 5.等待连接建立的结果,确认是否连接成功bool ret = socket->waitForConnected();if(!ret){QMessageBox::critical(this, "连接服务器失败", socket->errorString());exit(1);}
}Widget::~Widget()
{delete ui;
}void Widget::on_pushButton_clicked()
{// 1.获取到输入框中的内容const QString& text = ui->lineEdit->text();// 2.发送数据给服务器socket->write(text.toUtf8());// 3.把发的消息显示到界面上ui->listWidget->addItem("客户端说: " + text);// 4.清空输入框的内容ui->lineEdit->setText("");
}
服务器
Widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QTcpServer>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void processConnection();private:Ui::Widget *ui;QTcpServer* tcpServer;QString process(const QString request);
};
#endif // WIDGET_H
Widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QTcpSocket>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1.修改窗口标题this->setWindowTitle("服务器");// 2.创建 QTcpServer 的实例tcpServer = new QTcpServer(this);// 3.通过信号槽,指定如何处理函数connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);// 4.绑定并监听端口号,一定要在准备工作做完了(连接信号槽),才能进行绑定监听bool ret = tcpServer->listen(QHostAddress::Any, 9090);if(!ret){QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());exit(1);}
}Widget::~Widget()
{delete ui;
}void Widget::processConnection()
{// 1.通过 tcpServer 拿到一个 socket 对象,通过这个对象来和客户端进行通信QTcpSocket* clientSocket = tcpServer->nextPendingConnection();QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线!";ui->listWidget->addItem(log);// 2.通过信号槽,来处理客户端发来的请求情况connect(clientSocket, &QTcpSocket::readyRead, this, [=](){// a)读取请求数据,此处 readAll 返回的是 QByteArray, 通过赋值转成 QStringQString request = clientSocket->readAll();// b)根据请求处理响应const QString& response = process(request);// c)把响应写回到客户端clientSocket->write(response.toUtf8());// d)把上述信息记录到日志中QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "]"+ " req: " + request + ", resp: " + response;ui->listWidget->addItem(log);});// 3.通过信号槽,来处理客户端断开连接的情况connect(clientSocket, &QTcpSocket::disconnected, this, [=](){// a)把断开连接的信息通过日志显示出来QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端下线!";ui->listWidget->addItem(log);// b)手动释放 clientSocket 直接使用 delete 是下策,使用 deleteLater 更加合适// delete clientSocket;clientSocket->deleteLater();});
}QString Widget::process(const QString request)
{return request;
}