4.2基于TCP的Qt网络通信
在标准C++没有提供专门用于套接字通信的类,所以只能使用操作系统提供的基于C的API函数,基于这些C的API函数我们也可以封装自己的C++类 C++套接字类的封装(见4.1)。但是Qt就不一样了,它是C++的一个框架并且里边提供了用于套接字通信的类(TCP、UDP)这样就使得我们的操作变得更加简单了(当然,在Qt中使用标准C的API进行套接字通信也是完全没有问题的)。下面,给大家讲一下如果使用相关类的进行TCP通信。
使用Qt提供的类进行基于TCP的套接字通信需要用到两个类:
QTcpServer:服务器类,用于监听客户端连接以及和客户端建立连接。
QTcpSocket:通信的套接字类,客户端、服务器端都需要使用。
这两个套接字通信类都属于网络模块network。1. QTcpServer
QTcpServer类用于监听客户端连接以及和客户端建立连接,在使用之前先介绍一下这个类提供的一些常用API函数:
1.1 公共成员函数
构造函数
QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR);给监听的套接字设置监听
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0); // 判断当前对象是否在监听, 是返回true,没有监听返回false bool QTcpServer::isListening() const; // 如果当前对象正在监听返回监听的服务器地址信息, 否则返回 QHostAddress::Null QHostAddress QTcpServer::serverAddress() const; // 如果服务器正在侦听连接,则返回服务器的端口; 否则返回0 quint16 QTcpServer::serverPort() const参数:
address:通过类QHostAddress可以封装IPv4、IPv6格式的IP地址,QHostAddress::Any表示自动绑定
port:如果指定为0表示随机绑定一个可用端口。
返回值:绑定成功返回true,失败返回false
得到和客户端建立连接之后用于通信的QTcpSocket套接字对象,它是QTcpServer的一个子对象,当QTcpServer对象析构的时候会自动析构这个子对象,当然也可自己手动析构,建议用完之后自己手动析构这个通信的QTcpSocket对象。QTcpSocket *QTcpServer::nextPendingConnection();阻塞等待客户端发起的连接请求,不推荐在单线程程序中使用,建议使用非阻塞方式处理新连接,即使用信号 newConnection()
bool QTcpServer::waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR);参数:
msec:指定阻塞的最大时长,单位为毫秒(ms)
timeout:传出参数,如果操作超时timeout为true,没有超时timeout为false1.2 信号
当接受新连接导致错误时,将发射如下信号。socketError参数描述了发生的错误相关的信息。
[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError);每次有新连接可用时都会发出 newConnection() 信号。
[signal] void QTcpServer::newConnection();2. QTcpSocket
QTcpSocket是一个套接字通信类,不管是客户端还是服务器端都需要使用。在Qt中发送和接收数据也属于IO操作(网络IO),先来看一下这个类的继承关系:
2.1 公共成员函数
构造函数
QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR);连接服务器,需要指定服务器端绑定的IP和端口信息。
[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol); [virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);在Qt中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由Qt框架维护的一块内存。因此,调用了发送函数数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据,关于底层的相关操作是不需要使用者来维护的。
接收数据
// 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中 qint64 QIODevice::read(char *data, qint64 maxSize); // 指定可接收的最大字节数 maxSize,返回接收的字符串 QByteArray QIODevice::read(qint64 maxSize); // 将当前可用操作数据全部读出,通过返回值返回读出的字符串 QByteArray QIODevice::readAll();发送数据
// 发送指针 data 指向的内存中的 maxSize 个字节的数据 qint64 QIODevice::write(const char *data, qint64 maxSize); // 发送指针 data 指向的内存中的数据,字符串以 \0 作为结束标记 qint64 QIODevice::write(const char *data); // 发送参数指定的字符串 qint64 QIODevice::write(const QByteArray &byteArray);2.2 信号
在使用QTcpSocket进行套接字通信的过程中,如果该类对象发射出readyRead()信号,说明对端发送的数据达到了,之后就可以调用 read 函数接收数据了。
[signal] void QIODevice::readyRead();调用connectToHost()函数并成功建立连接之后发出connected()信号。
[signal] void QAbstractSocket::connected();在套接字断开连接时发出disconnected()信号。
[signal] void QAbstractSocket::disconnected();3. 通信流程
使用Qt提供的类进行套接字通信比使用标准C的API进行网络通信要简单(因为在内部进行了封装)原始的TCP通信流程 Qt中的套接字通信流程如下:
3.1 服务器端
3.1.1 通信流程
1.创建套接字服务器QTcpServer对象
2.通过QTcpServer对象设置监听,即:QTcpServer::listen()
3.基于QTcpServer::newConnection()信号检测是否有新的客户端连接
4.如果有新的客户端连接调用QTcpSocket *QTcpServer::nextPendingConnection()得到通信的套接字对象
5.使用通信的套接字对象QTcpSocket和客户端进行通信3.1.2 代码片段
服务器端的窗口界面如下图所示:
头文件
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_startServer_clicked(); void on_sendMsg_clicked(); private: Ui::MainWindow *ui; QTcpServer* m_server; QTcpSocket* m_tcp; };源文件
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setWindowTitle("TCP - 服务器"); // 创建 QTcpServer 对象 m_server = new QTcpServer(this); // 检测是否有新的客户端连接 connect(m_server, &QTcpServer::newConnection, this, [=]() { m_tcp = m_server->nextPendingConnection(); ui->record->append("成功和客户端建立了新的连接..."); m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20)); // 检测是否有客户端数据 connect(m_tcp, &QTcpSocket::readyRead, this, [=]() { // 接收数据 QString recvMsg = m_tcp->readAll(); ui->record->append("客户端Say: " + recvMsg); }); // 客户端断开了连接 connect(m_tcp, &QTcpSocket::disconnected, this, [=]() { ui->record->append("客户端已经断开了连接..."); m_tcp->deleteLater(); m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20)); }); }); } MainWindow::~MainWindow() { delete ui; } // 启动服务器端的服务按钮 void MainWindow::on_startServer_clicked() { unsigned short port = ui->port->text().toInt(); // 设置服务器监听 m_server->listen(QHostAddress::Any, port); ui->startServer->setEnabled(false); } // 点击发送数据按钮 void MainWindow::on_sendMsg_clicked() { QString sendMsg = ui->msg->toPlainText(); m_tcp->write(sendMsg.toUtf8()); ui->record->append("服务器Say: " + sendMsg); ui->msg->clear(); }3.2 客户端
3.2.1 通信流程
1.创建通信的套接字类QTcpSocket对象
2.使用服务器端绑定的IP和端口连接服务器QAbstractSocket::connectToHost()
3.使用QTcpSocket对象和服务器进行通信3.2.2 代码片段
客户端的窗口界面如下图所示:
头文件
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_connectServer_clicked(); void on_sendMsg_clicked(); void on_disconnect_clicked(); private: Ui::MainWindow *ui; QTcpSocket* m_tcp; };源文件
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setWindowTitle("TCP - 客户端"); // 创建通信的套接字对象 m_tcp = new QTcpSocket(this); // 检测服务器是否回复了数据 connect(m_tcp, &QTcpSocket::readyRead, [=]() { // 接收服务器发送的数据 QByteArray recvMsg = m_tcp->readAll(); ui->record->append("服务器Say: " + recvMsg); }); // 检测是否和服务器是否连接成功了 connect(m_tcp, &QTcpSocket::connected, this, [=]() { ui->record->append("恭喜, 连接服务器成功!!!"); m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20)); }); // 检测服务器是否和客户端断开了连接 connect(m_tcp, &QTcpSocket::disconnected, this, [=]() { ui->record->append("服务器已经断开了连接, ..."); ui->connectServer->setEnabled(true); ui->disconnect->setEnabled(false); }); } MainWindow::~MainWindow() { delete ui; } // 连接服务器按钮按下之后的处理动作 void MainWindow::on_connectServer_clicked() { QString ip = ui->ip->text(); unsigned short port = ui->port->text().toInt(); // 连接服务器 m_tcp->connectToHost(QHostAddress(ip), port); ui->connectServer->setEnabled(false); ui->disconnect->setEnabled(true); } // 发送数据按钮按下之后的处理动作 void MainWindow::on_sendMsg_clicked() { QString sendMsg = ui->msg->toPlainText(); m_tcp->write(sendMsg.toUtf8()); ui->record->append("客户端Say: " + sendMsg); ui->msg->clear(); } // 断开连接按钮被按下之后的处理动作 void MainWindow::on_disconnect_clicked() { m_tcp->close(); ui->connectServer->setEnabled(true); ui->disconnect->setEnabled(false); }
笔记
1.Qt套接字通信概述
tcp是流式传输协议
udp是报文形式
在c语言里tcp和udp通讯编程是最复杂的
tcp通信:
tcp服务器需要先创建套接字,然后再绑定设置监听,等待客户端的连接,连接成功之后接收和发送数据,最后关闭套接字
客户端是创建套接字,连接服务器,连接成功之后和服务器进行通信,最后关闭套接字
udp通信:
QTcpSocket类和QFile类都是继承自QIoDevice类
2.QTcpService类的常用API
父对象和父类是两码事,父类有继承关系,父对象没有
默认情况下listen函数有两个参数,这两个参数都有默认值,但是我们在使用的时候一定要给port指定一个固定的端口,(8000以上)
pending 待处理的,挂起的
3.QTcpSocket类的常用API
使用QTcpSocket这个类进行套接字通信的时候,不管是进行数据的读操作还是写操作,这个数据操作都不是直接操作网络中的数据,操作的是qt开辟的一块内存中本地的数据。
Socket = IP 地址 + 端口号
IP 地址:找到哪台电脑(如 192.168.1.100) 端口号:找到电脑上的哪个程序(如 8080) ↓ Socket = 192.168.1.100:8080两种常见类型
类型 类比 特点 TCP Socket 打电话 可靠、有序、有连接 UDP Socket 发短信/广播 快速、不可靠、无连接 通信流程
服务器端
// ① 创建 Socket // ② 绑定 IP 和端口(bind) // ③ 监听连接(listen) // ④ 接受客户端连接(accept) // ⑤ 收发数据(send/recv) // ⑥ 关闭连接(close)客户端// ① 创建 Socket // ② 连接服务器(connect) // ③ 收发数据(send/recv) // ④ 关闭连接(close)Qt 中的 Socket
#include <QTcpSocket> // 客户端连接 QTcpSocket *socket = new QTcpSocket(this); socket->connectToHost("127.0.0.1", 8080); // 连接服务器 // 发送数据 socket->write("Hello Server"); // 接收数据 connect(socket, &QTcpSocket::readyRead, this, [=](){ QByteArray data = socket->readAll(); qDebug() << "收到:" << data; });总结
Socket 是网络通信的端点,由 IP 地址 + 端口号组成。就像打电话:IP 是对方号码,端口是分机号,Socket 让两个程序能通过网络互相收发数据。TCP 像打电话(可靠),UDP 像发短信(快但可能丢)。
4.Qt中基于tcp进行套接字通信的流程
5.服务器端窗口搭建
两个文本框窗口,第一个窗口用来显示我们聊天的历史信息,第二个窗口用来显示要发送的数据,当点击发送数据按钮后输入框里的数据就被发送到客户端,并且信息会被记录到上边的历史框中,连接状态加一个label控件。
第一部分:拖拽一个widget里边放三个控件,第一个控件是label,第二个控件是line edit,还有一个按钮,然后做水平布局。
第二部分是一个组框,在组框里放一个多行文本编辑框text edit
第三部分:复制一份组框
第四部分:拖拽一个widget,放置一个按钮,放两个弹簧,水平布局
最外层设置垂直布局
6.服务器端套接字通信处理
需要加载一个模块network
头文件加QTcpServer类
启动监听之后让按钮变成不可用状态
将#include <QTcpSocket>从.cpp移到.h中
7.服务器端状态栏的状态处理
添加资源文件
添加前缀,添加文件
8.客户端的通信流程处理
拷贝一份QtServer项目改名为QtClient
9.程序测试
客户端代码:
一、头文件
mainwindow.h#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QTcpSocket> // TCP 套接字类 #include <QLabel> // 标签类(状态栏用) QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; private slots: void on_sendMsg_clicked(); // 发送按钮槽函数 void on_connect_clicked(); // 连接按钮槽函数 void on_disconnect_clicked(); // 断开按钮槽函数 private: Ui::MainWindow *ui; QTcpSocket* m_tcp; // 客户端通信套接字 QLabel* m_status; // 状态栏图标标签 }; #endif // MAINWINDOW_H二、源文件
mainwindow.cpp#include "mainwindow.h" #include "ui_mainwindow.h" #include <QHostAddress> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 初始化 UI 界面 ui->port->setText("8899"); // 设置默认端口号 ui->ip->setText("127.0.0.1"); // 设置默认 IP setWindowTitle("客户端"); // 设置窗口标题 ui->disconnect->setDisabled(true); // 断开按钮初始禁用 // ============================================================ // 1. 创建 QTcpSocket 通信套接字 // ============================================================ m_tcp = new QTcpSocket(this); // 创建客户端套接字对象 // parent = this,MainWindow 销毁时自动清理 // ★ 只需要创建一次,断开连接时不要销毁它! // ============================================================ // 2. 创建状态栏图标 // ============================================================ m_status = new QLabel; // 创建标签对象,用于显示连接状态图标 m_status->setPixmap(QPixmap(":/80.jpg").scaled(20, 20)); // 设置初始图标:灰色(未连接) ui->statusbar->addWidget(new QLabel("连接状态:")); // 状态栏添加文字 ui->statusbar->addWidget(m_status); // 状态栏添加图标 // ============================================================ // 3. 接收数据:服务器发来消息时触发 // ============================================================ connect(m_tcp, &QTcpSocket::readyRead, this, [=](){ QByteArray data = m_tcp->readAll(); // 读取所有收到的数据 ui->record->append("服务器say:" + data); // 显示到记录框 }); // ============================================================ // 4. 断开连接:连接断开时触发 // ============================================================ connect(m_tcp, &QTcpSocket::disconnected, this, [=](){ ui->record->append("已和服务器断开连接..."); // 显示断开提示 m_status->setPixmap(QPixmap(":/80.jpg").scaled(20, 20)); // 图标变灰 ui->connect->setDisabled(false); // 启用"连接"按钮 ui->disconnect->setDisabled(true); // 禁用"断开"按钮 // ★ 修正:不销毁 m_tcp! // 不调用 deleteLater(),不置 nullptr // 保留 m_tcp 对象,下次还能复用 }); // ============================================================ // 5. 连接成功:成功连上服务器时触发 // ============================================================ connect(m_tcp, &QTcpSocket::connected, this, [=](){ ui->record->append("已经成功连接到了服务器..."); // 显示连接成功提示 m_status->setPixmap(QPixmap(":/81.jpg").scaled(20, 20)); // 图标变绿 ui->connect->setDisabled(true); // 禁用"连接"按钮 ui->disconnect->setDisabled(false); // 启用"断开"按钮 }); } MainWindow::~MainWindow() { delete ui; // 释放 UI 对象 // m_tcp 由 Qt 对象树自动管理 } // ============================================================ // ★ 发送按钮 // ============================================================ void MainWindow::on_sendMsg_clicked() { QString msg = ui->msg->toPlainText(); // 获取输入框内容 // ★ 判空 + 判断连接状态 if (m_tcp == nullptr || m_tcp->state() != QAbstractSocket::ConnectedState) { ui->record->append("未连接到服务器,无法发送!"); return; } m_tcp->write(msg.toUtf8()); // 发送数据(QString → QByteArray) ui->record->append("客户端say:" + msg); // 显示自己发送的消息 ui->msg->clear(); // 清空输入框 } // ============================================================ // ★ 连接按钮 // ============================================================ void MainWindow::on_connect_clicked() { QString ip = ui->ip->text(); // 获取 IP 地址 unsigned short port = ui->port->text().toUShort(); // 获取端口号 m_tcp->connectToHost(QHostAddress(ip), port); // ★ 复用 m_tcp 对象发起连接 // 断开后再次点击"连接",这里仍然安全 // 因为 m_tcp 没有被销毁 ui->record->append("正在连接服务器..."); // 提示正在连接 } // ============================================================ // ★ 断开按钮 // ============================================================ void MainWindow::on_disconnect_clicked() { m_tcp->close(); // 关闭连接(不是销毁对象!) // close() 后 m_tcp 对象仍然存在,可以再次 connectToHost ui->record->append("主动断开连接"); // 提示 ui->connect->setDisabled(false); // 启用"连接"按钮 ui->disconnect->setDisabled(true); // 禁用"断开"按钮 m_status->setPixmap(QPixmap(":/80.jpg").scaled(20, 20)); // 图标变灰 }服务器代码:
一、头文件
mainwindow.h#ifndef MAINWINDOW_H // 防止头文件重复包含 #define MAINWINDOW_H // 定义头文件保护宏 #include <QMainWindow> // Qt 主窗口类 #include <QTcpSocket> // TCP 套接字类(用于与客户端通信) #include <QTcpServer> // TCP 服务器类(用于监听客户端连接) #include <QLabel> // 标签类(状态栏显示用) QT_BEGIN_NAMESPACE // Qt 命名空间开始 namespace Ui { class MainWindow; } // 前置声明 Ui::MainWindow 类 QT_END_NAMESPACE // Qt 命名空间结束 class MainWindow : public QMainWindow // 定义 MainWindow 类,继承 QMainWindow { Q_OBJECT // Qt 元对象系统宏,启用信号槽 public: explicit MainWindow(QWidget *parent = nullptr); // 构造函数 ~MainWindow() override; // 析构函数 private slots: void on_setListen_clicked(); // 槽函数:点击"监听"按钮 void on_sendMsg_clicked(); // 槽函数:点击"发送"按钮 private: Ui::MainWindow *ui; // UI 对象指针 QTcpServer* m_s; // 服务器对象(负责监听) QTcpSocket* m_tcp; // 通信套接字(与当前连接的客户端通信) QLabel* m_status; // 状态栏图标 }; #endif // MAINWINDOW_H // 头文件保护结束二、源文件
mainwindow.cpp#include "mainwindow.h" #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 初始化 UI 界面 ui->port->setText("8899"); // 默认端口号 8899 setWindowTitle("服务器"); // 设置窗口标题 // ============================================================ // 1. 创建服务器对象 // ============================================================ m_s = new QTcpServer(this); // 创建 QTcpServer 对象 // parent = this,MainWindow 销毁时自动清理 // ============================================================ // 2. 初始化通信套接字为空 // ============================================================ m_tcp = nullptr; // 还没有客户端连接,所以为 nullptr // ============================================================ // 3. 创建状态栏图标 // ============================================================ m_status = new QLabel; // 创建标签对象 m_status->setPixmap(QPixmap(":/80.jpg").scaled(20, 20)); // 设置初始图标:灰色(未连接) // scaled(20,20):缩放为 20×20 像素 ui->statusbar->addWidget(new QLabel("连接状态:")); // 状态栏添加文字标签 ui->statusbar->addWidget(m_status); // 状态栏添加状态图标 // ============================================================ // 4. ★ 核心:有客户端连接时触发 // ============================================================ connect(m_s, &QTcpServer::newConnection, this, [=](){ // newConnection 信号:有新的客户端连接请求时发射 m_tcp = m_s->nextPendingConnection(); // ★ 获取新连接的客户端套接字 // 每个客户端连接对应一个 QTcpSocket // 通过这个套接字与客户端收发数据 m_status->setPixmap(QPixmap(":/81.jpg").scaled(20, 20)); // 图标变绿色(已连接) ui->record->append("客户端已连接!"); // 显示连接提示 // ============================================================ // 5. 接收数据:客户端发来消息时触发 // ============================================================ connect(m_tcp, &QTcpSocket::readyRead, this, [=](){ // readyRead 信号:套接字缓冲区有数据可读时发射 QByteArray data = m_tcp->readAll(); // 读取所有收到的数据 ui->record->append("客户端say:" + data); // 显示到记录框 }); // ============================================================ // 6. 断开连接:客户端断开时触发 // ============================================================ connect(m_tcp, &QTcpSocket::disconnected, this, [=](){ // disconnected 信号:连接断开时发射 ui->record->append("客户端已断开"); // 显示断开提示 m_tcp->close(); // 关闭套接字 m_tcp->deleteLater(); // 延迟销毁套接字对象 m_tcp = nullptr; // 置空指针,防止野指针 m_status->setPixmap(QPixmap(":/80.jpg").scaled(20, 20)); // 图标变灰(未连接) }); }); } MainWindow::~MainWindow() { delete ui; // 释放 UI 对象 // m_s 和 m_status 由对象树自动管理 } // ============================================================ // ★ 发送按钮 // ============================================================ void MainWindow::on_sendMsg_clicked() { QString msg = ui->msg->toPlainText(); // 获取输入框内容 // ★ 判空保护 if (m_tcp == nullptr) { ui->record->append("没有客户端连接,无法发送!"); return; } m_tcp->write(msg.toUtf8()); // 发送数据 // msg.toUtf8():QString 转为 QByteArray(UTF-8 编码) ui->record->append("服务器say:" + msg); // 显示自己发送的消息 } // ============================================================ // ★ 监听按钮 // ============================================================ void MainWindow::on_setListen_clicked() { unsigned short port = ui->port->text().toUShort(); // 获取端口号 // toUShort():QString → unsigned short m_s->listen(QHostAddress::Any, port); // ★ 开始监听 // QHostAddress::Any:监听所有网络接口 // port:监听的端口号 ui->setListen->setDisabled(true); // 禁用监听按钮,防止重复监听 }客户端 vs 服务器对比
对比项 客户端 服务器 核心类 QTcpSocketQTcpServer+QTcpSocket启动方式 connectToHost(ip, port)listen(Any, port)连接成功信号 connectednewConnection获取连接 — nextPendingConnection()收数据 readyRead+readAll()readyRead+readAll()发数据 write()write()断开 close()/disconnecteddisconnected重连策略 ✅ 复用 m_tcp 新连接新建 m_tcp 完整通信流程
【服务器】 【客户端】 │ │ │ ① 创建 QTcpServer │ ① 创建 QTcpSocket │ ② m_tcp = nullptr │ ② 状态栏灰色 │ ③ 状态栏灰色 │ │ │ │ ④ listen(8899) │ │ ← 等待连接... │ │ │ ④ connectToHost(127.0.0.1, 8899) │ ←───────── TCP 三次握手 ─────────│ │ │ │ ⑤ newConnection 触发 │ ⑤ connected 触发 │ ⑥ m_tcp = nextPendingConnection() │ ⑥ 图标变绿 │ ⑦ 图标变绿 │ │ │ │ ←───────── 客户端 write ─────────│ │ ⑧ readyRead 触发 │ │ ⑨ readAll() → 显示 │ │ │ │ ⑩ write("回复") ─────────────────→│ │ │ ⑧ readyRead 触发 │ │ ⑨ readAll() → 显示 │ │ │ ←───────── 客户端 close ─────────│ │ ⑪ disconnected 触发 │ ⑪ disconnected 触发 │ ⑫ 清理 m_tcp │ ⑫ 图标变灰 │ ⑬ 图标变灰 │ ⑬ 保留 m_tcp,可重连完整程序运行流程
一、启动阶段
服务器程序启动
main() 执行 ↓ 创建 QApplication ↓ 创建 MainWindow(服务器窗口) ↓ MainWindow 构造函数执行: ① m_s = new QTcpServer(this) → 创建服务器对象 ② m_tcp = nullptr → 初始化套接字为空 ③ m_status = new QLabel → 创建状态图标 ④ 状态栏显示灰色图标 [未连接] ⑤ 默认端口号 8899 ⑥ connect 连接信号槽 ↓ 窗口显示 ↓ 进入事件循环 a.exec() ↓ 等待用户操作...客户端程序启动
main() 执行 ↓ 创建 QApplication ↓ 创建 MainWindow(客户端窗口) ↓ MainWindow 构造函数执行: ① m_tcp = new QTcpSocket(this) → 创建套接字对象 ② m_status = new QLabel → 创建状态图标 ③ 状态栏显示灰色图标 [未连接] ④ 默认 IP: 127.0.0.1,端口: 8899 ⑤ 连接按钮启用,断开按钮禁用 ⑥ connect 连接信号槽 ↓ 窗口显示 ↓ 进入事件循环 a.exec() ↓ 等待用户操作...二、建立连接阶段
【服务器】 【客户端】 ↓ ↓ 用户点击"监听"按钮 用户点击"连接"按钮 ↓ ↓ on_setListen_clicked() on_connect_clicked() ↓ ↓ m_s->listen(Any, 8899) m_tcp->connectToHost(127.0.0.1, 8899) ↓ ↓ 监听按钮禁用 发起 TCP 连接请求 ↓ ↓ 等待客户端连接... ↓ ↓ ┌───────────┘ ↓ TCP 三次握手 ↓ ┌───────────────────────┘ ↓ newConnection 信号触发 ↓ m_tcp = nextPendingConnection() ↓ 图标变绿 [已连接] ↓ 显示"客户端已连接!" ↓ 连接 readyRead 和 disconnected 信号 │ └───────────────────────────────→ connected 信号触发 ↓ 图标变绿 [已连接] ↓ 显示"连接成功" ↓ 连接按钮禁用,断开按钮启用三、通信阶段
客户端 → 服务器
【客户端】 用户在输入框输入消息 ↓ 点击"发送"按钮 ↓ on_sendMsg_clicked() ↓ 判断 m_tcp 是否连接 ↓ m_tcp->write(msg.toUtf8()) ↓ 显示"客户端say:xxx" ↓ 数据通过网络传输 ↓ 【服务器】 readyRead 信号触发 ↓ m_tcp->readAll() ↓ 显示"客户端say:xxx"服务器 → 客户端
【服务器】 用户在输入框输入消息 ↓ 点击"发送"按钮 ↓ on_sendMsg_clicked() ↓ 判断 m_tcp 是否为空 ↓ m_tcp->write(msg.toUtf8()) ↓ 显示"服务器say:xxx" ↓ 数据通过网络传输 ↓ 【客户端】 readyRead 信号触发 ↓ m_tcp->readAll() ↓ 显示"服务器say:xxx"四、断开连接阶段
客户端主动断开
【客户端】 点击"断开"按钮 ↓ on_disconnect_clicked() ↓ m_tcp->close() ↓ 图标变灰 [未连接] ↓ 连接按钮启用,断开按钮禁用 ↓ 【服务器】 disconnected 信号触发 ↓ 显示"客户端已断开" ↓ m_tcp->close() ↓ m_tcp->deleteLater() → 销毁套接字 ↓ m_tcp = nullptr ↓ 图标变灰 [未连接]服务器主动关闭
【服务器】 关闭窗口 ↓ m_s 销毁 ↓ 所有连接断开 ↓ 【客户端】 disconnected 信号触发 ↓ 显示"已和服务器断开连接" ↓ 图标变灰 ↓ 连接按钮启用,断开按钮禁用五、重连阶段(客户端特色)
【客户端】 点击"断开" → m_tcp 对象还在(复用) ↓ 再次点击"连接" ↓ on_connect_clicked() ↓ m_tcp->connectToHost(...) ← 复用同一个对象,安全! ↓ 连接成功 → connected 触发 ↓ 图标变绿完整时序图
时间轴 → 【服务器】 【客户端】 │ │ │ 启动程序 │ 启动程序 │ 灰色图标 │ 灰色图标 │ │ │ 点击监听 │ │ listen() │ │ │ 点击连接 │ │ connectToHost() │ ←──握手────→ │ │ │ │ newConnection│ connected │ 绿色图标 │ 绿色图标 │ │ │ │ write("你好") │ readyRead │ │ readAll() │ │ │ │ write("Hi") │ │ │ readyRead │ │ readAll() │ │ │ │ close() │ disconnected │ disconnected │ 清理 m_tcp │ 灰色图标 │ 灰色图标 │ m_tcp 保留 │ │ │ │ 再次 connectToHost │ newConnection│ connected │ 绿色图标 │ 绿色图标 │ │ ...循环事件循环的作用
整个程序运行在事件循环中(a.exec()): while (true) { 等待事件 ↓ 有事件发生? ├── 鼠标点击 → 调用槽函数 ├── 键盘输入 → 更新界面 ├── 网络数据 → readyRead 信号 ├── 连接请求 → newConnection 信号 ├── 连接断开 → disconnected 信号 └── ... ↓ 处理完毕,继续等待 }总结
整个程序运行在 Qt 事件循环中。服务器 listen 等待 → 客户端 connectToHost 连接 → 双方通过信号槽收发数据(write 发送,readyRead 触发读取)→ disconnected 断开。客户端复用套接字支持重连,服务器每次连接创建新套接字。
deleteLater 详解
deleteLater()是 QObject 的方法,用于安全地延迟销毁对象。核心作用
obj->deleteLater(); // 不是立即 delete! // 而是在"事件循环的下一次迭代"中销毁为什么需要延迟删除?
场景:在信号槽中销毁自己
// ❌ 错误:在槽函数中直接 delete 自己 connect(btn, &QPushButton::clicked, this, [=](){ delete this; // ← 危险!槽函数还没执行完,对象就被销毁了 // 后面的代码访问 this → 崩溃! }); // ✅ 正确:用 deleteLater connect(btn, &QPushButton::clicked, this, [=](){ this->deleteLater(); // 告诉 Qt:"等当前事件处理完,再销毁我" // 安全! });执行时机
当前事件处理中: obj->deleteLater(); // ① 只是"标记"要删除 // 继续执行后面的代码... // ② 此时 obj 还在,可以安全使用 当前事件处理完毕: Qt 事件循环检查删除队列 → 发现 obj 被标记 → delete obj // ③ 真正销毁类比理解
直接 delete = 当场枪毙 deleteLater = 判死刑,秋后问斩 "秋后" = 当前事件处理完毕 "问斩" = 真正执行 delete常用场景
场景1:槽函数中销毁发送者
connect(sender, &Sender::finished, this, [=](){ sender->deleteLater(); // 信号处理完毕后再销毁,安全 });场景2:关闭窗口
connect(btn, &QPushButton::clicked, this, [=](){ this->deleteLater(); // 替代 this->delete this });场景3:清理临时对象
QTcpSocket *tempSocket = new QTcpSocket; // ... 使用 ... tempSocket->deleteLater(); // 用完稍后自动清理与
delete的对比
特性 delete objobj->deleteLater()执行时机 立即 事件循环下一次迭代 是否阻塞 是 否 安全性 在槽函数中危险 ✅ 安全 使用场景 对象不再使用 对象可能还在被引用 你代码中为什么出问题?
connect(m_tcp, &QTcpSocket::disconnected, this, [=](){ m_tcp->close(); m_tcp->deleteLater(); // ← 对象被销毁了 m_tcp = NULL; // ← 指针置空 }); // 之后点击"连接": void on_connect_clicked() { m_tcp->connectToHost(...); // ← m_tcp 是 nullptr!崩溃! }总结
deleteLater()是"延迟删除":标记对象要销毁,等当前事件处理完后才真正delete。它比直接delete安全,适合在信号槽中销毁对象。你代码中崩溃的原因不是deleteLater本身,而是销毁后没有重新创建m_tcp就重连。
10.搭建发送文件的客户端窗口界面
在主窗口中做一些初始化
连接服务器
13.客户端在子线程中发送文件
14.服务器端的多线程处理
15.程序测试
让输出显示到控制台而不是qt
16解决通信的套接字对象无法在子线程中工作的问题
代码:
SendFileClient.pro
QT += widgets network CONFIG += c++17 console # You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ main.cpp \ mainwindow.cpp \ sendfile.cpp HEADERS += \ mainwindow.h \ sendfile.h FORMS += \ mainwindow.ui # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target
mainwindow.h(发送端)#ifndef MAINWINDOW_H #define MAINWINDOW_H // 防止重复包含 #include <QMainWindow> // Qt 主窗口类 QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE // 前置声明 Ui::MainWindow class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; signals: void startConnect(unsigned short, QString); // 信号:请求连接服务器(端口,IP) void sendFile(QString path); // 信号:请求发送文件(路径) private slots: void on_connectServer_clicked(); // 槽:点击"连接服务器"按钮 void on_setFile_clicked(); // 槽:点击"选择文件"按钮 void on_sendFile_clicked(); // 槽:点击"发送文件"按钮 private: Ui::MainWindow *ui; // UI 对象指针 }; #endif // MAINWINDOW_H
sendfile.h#ifndef SENDFILE_H // 【预处理】防止头文件重复包含 #define SENDFILE_H // 定义头文件保护宏 #include <QObject> // 【头文件】Qt对象基类,支持信号槽和对象树 #include <QFileInfo> // 【头文件】文件信息类,用于获取文件名、大小、修改时间等 #include <QTcpSocket> // 【头文件】TCP套接字类,负责网络数据传输 // 【类定义】发送文件任务类,继承QObject以便使用moveToThread()移动到子线程 // 设计思想:将业务逻辑与界面分离,任务对象可以在任意线程执行 class SendFile : public QObject { Q_OBJECT // 【Qt宏】启用元对象特性,支持信号槽 public: // 【构造函数】显式构造函数,parent用于对象树管理 explicit SendFile(QObject *parent = nullptr); // 【成员函数】连接服务器(由信号触发,在子线程中执行) // 参数:port - 服务器端口号,ip - 服务器IP地址 // 执行环境:子线程(通过moveToThread()移动到子线程) void connectServer(unsigned short port, QString ip); // 【成员函数】发送文件(由信号触发,在子线程中执行) // 参数:path - 要发送的文件路径 // 执行环境:子线程 void sendFile(QString path); signals: // 【信号】连接成功时发射,通知主线程 void connectOK(); // 【信号】连接断开时发射,通知主线程清理资源 void gameover(); // 【信号】发送进度,参数为0-100的百分比 // 主线程接收后更新进度条 void curPercent(int num); private: QTcpSocket* m_tcp; // 【私有成员】TCP套接字指针,用于与服务器通信 // 注意:在子线程中创建和使用,需确保线程安全 }; #endif // SENDFILE_H
sendfile.cpp#include "sendfile.h" // 【包含】自己的头文件 #include <QFile> // 【包含】文件操作类,用于读写文件 #include <QHostAddress> // 【包含】IP地址类,用于解析和表示IP地址 #include <QDebug> // 【包含】调试输出类,打印日志信息 // ============================================================ // ★ 构造函数 // ============================================================ SendFile::SendFile(QObject *parent) : QObject(parent) // 调用基类构造函数,建立对象树关系 { m_tcp = nullptr; // 【初始化】套接字指针置空,表示还未连接 } // ============================================================ // ★ 连接服务器(在子线程中执行) // ============================================================ void SendFile::connectServer(unsigned short port, QString ip) { // ---------- 【步骤1】清理旧连接 ---------- if (m_tcp != nullptr) // 如果之前有连接 { m_tcp->close(); // 关闭套接字连接 m_tcp->deleteLater(); // 延迟销毁(安全删除Qt对象) m_tcp = nullptr; // 置空指针,防止野指针 } // ---------- 【步骤2】创建新连接 ---------- m_tcp = new QTcpSocket; // 在子线程中创建套接字(无父对象) // 注意:在子线程中创建的对象,其事件循环在子线程 m_tcp->connectToHost(QHostAddress(ip), port); // 异步连接服务器 // QHostAddress(ip):将字符串IP转换为Qt的IP地址对象 // port:目标端口号 // ---------- 【步骤3】连接成功信号 ---------- connect(m_tcp, &QTcpSocket::connected, this, &SendFile::connectOK); // 当套接字连接成功时,发射connectOK信号通知主线程 // 这种连接方式使用Qt::AutoConnection,根据线程自动选择连接类型 // ---------- 【步骤4】断开连接处理 ---------- connect(m_tcp, &QTcpSocket::disconnected, this, [=](){ // Lambda表达式捕获this和所有局部变量 // 当连接断开时执行清理工作 m_tcp->close(); // 关闭套接字 m_tcp->deleteLater(); // 延迟销毁套接字对象 m_tcp = nullptr; // 置空指针 emit gameover(); // 发射断开信号,通知主线程 }); } // ============================================================ // ★ 发送文件(在子线程中执行) // ============================================================ void SendFile::sendFile(QString path) { // ---------- 【验证】检查连接状态 ---------- if (m_tcp == nullptr || m_tcp->state() != QAbstractSocket::ConnectedState) // 如果套接字为空 或 未处于连接状态 { qDebug() << "未连接到服务器!"; // 输出调试信息 return; // 直接返回 } // ---------- 【打开文件】 ---------- QFile file(path); // 创建文件对象 if (!file.open(QIODevice::ReadOnly)) // 以只读模式打开 { qDebug() << "无法打开文件:" << path; return; // 打开失败则退出 } // ---------- 【获取文件信息】 ---------- QFileInfo info(path); // 文件信息对象 qint64 fileSize = info.size(); // 文件总大小(字节) QString fileName = info.fileName(); // 文件名(不含路径) // ============================================================ // ① 【发送文件名】 // ============================================================ m_tcp->write(fileName.toUtf8() + "\n"); // 将QString转为UTF-8字节数组,末尾加换行符 m_tcp->flush(); // 强制立即发送缓冲区数据 // ============================================================ // ② 【发送文件大小】 // ============================================================ m_tcp->write((char*)&fileSize, 8); // 将qint64的地址转为char*,发送8字节 // 注意:大小端问题!跨平台通信需要统一字节序(使用htonl/ntohl) m_tcp->flush(); // ============================================================ // ③ 【分块发送文件内容】 // ============================================================ qint64 sentSize = 0; // 已发送字节数(每次调用sendFile都会重置为0) while (!file.atEnd()) // 循环直到文件末尾 { QByteArray data = file.read(64 * 1024); // 每次读取64KB m_tcp->write(data); // 发送数据块 m_tcp->flush(); // 强制立即发送 sentSize += data.size(); // 累加已发送大小 // 计算进度百分比(注意浮点数运算) int percent = (int)((double)sentSize / fileSize * 100); emit curPercent(percent); // 发射进度信号 } file.close(); // 关闭文件 qDebug() << "文件发送完成:" << fileName; }
mainwindow.cpp(发送端)#include "mainwindow.h" #include "ui_mainwindow.h" #include <QThread> #include <QMessageBox> #include "sendfile.h" #include <QFileDialog> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); ui->ip->setText("127.0.0.1"); // 默认 IP ui->port->setText("8989"); // 默认端口 ui->progressBar->setRange(0, 100); // 进度条范围 0~100 ui->progressBar->setValue(0); // 初始进度 0 // ============================================================ // 创建子线程和任务对象 // ============================================================ QThread* t = new QThread; // 创建线程对象 SendFile* worker = new SendFile; // 创建任务对象 worker->moveToThread(t); // ★ 把任务对象移到子线程 // 之后 worker 的槽函数都在 t 线程中执行 // ============================================================ // 连接信号槽 // ============================================================ connect(this, &MainWindow::sendFile, worker, &SendFile::sendFile); // 主窗口发 sendFile → worker 的 sendFile 在子线程执行 connect(this, &MainWindow::startConnect, worker, &SendFile::connectServer); // 主窗口发 startConnect → worker 的 connectServer 在子线程执行 connect(worker, &SendFile::connectOK, this, [=](){ QMessageBox::information(this, "连接服务器", "已经成功连接了服务器,恭喜!"); }); // worker 连接成功 → 主线程弹窗 connect(worker, &SendFile::gameover, this, [=](){ t->quit(); // 退出线程事件循环 t->wait(); // 等待线程结束 t->deleteLater(); // 销毁线程对象 worker->deleteLater(); // 销毁任务对象 }); // worker 断开连接 → 清理资源 connect(worker, &SendFile::curPercent, ui->progressBar, &QProgressBar::setValue); // worker 发进度 → 更新进度条 t->start(); // 启动子线程 } MainWindow::~MainWindow() { delete ui; } // ============================================================ // ★ 点击"连接服务器" // ============================================================ void MainWindow::on_connectServer_clicked() { QString ip = ui->ip->text(); unsigned short port = ui->port->text().toUShort(); emit startConnect(port, ip); // 发射信号 → 子线程执行 connectServer } // ============================================================ // ★ 点击"选择文件" // ============================================================ void MainWindow::on_setFile_clicked() { QString path = QFileDialog::getOpenFileName(); // 打开文件选择对话框 if (path.isEmpty()) { QMessageBox::warning(this, "打开文件", "选择的文件路径不能为空!"); return; } ui->filePath->setText(path); // 显示选择的路径 } // ============================================================ // ★ 点击"发送文件" // ============================================================ void MainWindow::on_sendFile_clicked() { emit sendFile(ui->filePath->text()); // 发射信号 → 子线程执行 sendFile }SendFileServer.pro
QT += widgets network CONFIG += c++17 console # You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ main.cpp \ mainwindow.cpp \ recvfile.cpp HEADERS += \ mainwindow.h \ recvfile.h FORMS += \ mainwindow.ui # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target接收端
recvfile.cpp#include "recvfile.h" #include <QFile> #include <QDebug> RecvFile::RecvFile(QTcpSocket* tcp, QObject *parent) : QThread(parent) { m_tcp = tcp; // 保存传入的套接字 } void RecvFile::run() { qDebug() << "接收线程:" << QThread::currentThread(); // 打印当前线程 // ============================================================ // 创建文件(临时名,收到头部后改名) // ============================================================ QFile *file = new QFile("recv.txt"); // 创建文件对象(初始文件名) file->open(QFile::WriteOnly); // 以只写模式打开 // ============================================================ // 定义接收状态变量(lambda 捕获) // ============================================================ QString fileName; // 接收的文件名 qint64 totalSize = 0; // 文件总大小 qint64 recvSize = 0; // 已接收大小 bool headerReceived = false; // 是否已解析头部 // ============================================================ // 连接 readyRead 信号:有数据到达时触发 // ============================================================ connect(m_tcp, &QTcpSocket::readyRead, this, [=]() mutable { // mutable:允许修改按值捕获的变量 QByteArray data = m_tcp->readAll(); // 读取所有数据 // ---------- 第一次接收:解析头部 ---------- if (!headerReceived) // 还没解析过头部 { int newlineIndex = data.indexOf('\n'); // 查找文件名分隔符 if (newlineIndex != -1) // 找到了 { fileName = QString::fromUtf8(data.left(newlineIndex)); // 解析文件名 data = data.mid(newlineIndex + 1); // 剩余数据 if (data.size() >= 8) // 至少有 8 字节的大小数据 { memcpy(&totalSize, data.constData(), 8); // 拷贝 8 字节到 totalSize data = data.mid(8); // 剩余数据 headerReceived = true; // 头部解析完成 // 用真实文件名重新创建文件 file->close(); file->deleteLater(); file = new QFile(fileName); file->open(QFile::WriteOnly); qDebug() << "接收文件:" << fileName << " 大小:" << totalSize; } } } // ---------- 写入文件内容 ---------- if (!data.isEmpty()) // 有数据 { file->write(data); // 写入文件 recvSize += data.size(); // 累加已接收大小 } // ---------- 判断是否接收完毕 ---------- if (headerReceived && totalSize > 0 && recvSize >= totalSize) // 头部已解析 + 有大小 + 接收完毕 { qDebug() << "文件接收完成!"; m_tcp->close(); // 关闭套接字 m_tcp->deleteLater(); // 销毁套接字 file->close(); // 关闭文件 file->deleteLater(); // 销毁文件对象 emit over(); // 发射完成信号 } }); exec(); // 进入事件循环 }接收端
mainwindow.cpp#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> #include <QTcpSocket> #include <QHostAddress> #include "recvfile.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); m_s = new QTcpServer(this); // 创建服务器对象 connect(m_s, &QTcpServer::newConnection, this, [=](){ // 有客户端连接时触发 QTcpSocket *tcp = m_s->nextPendingConnection(); // 获取客户端套接字 RecvFile* subThread = new RecvFile(tcp); // 创建接收线程 subThread->start(); // 启动线程 connect(subThread, &RecvFile::over, this, [=](){ // 接收完成 subThread->exit(); subThread->wait(); subThread->deleteLater(); QMessageBox::information(this, "文件接收", "文件接收完毕!"); }); }); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_setListen_clicked() { unsigned short port = ui->port->text().toUShort(); m_s->listen(QHostAddress::Any, port); // 开始监听 }recvfile.h
#include "recvfile.h" #include <QFile> #include <QDebug> // ============================================================ // ★ 构造函数 // ============================================================ RecvFile::RecvFile(QTcpSocket* tcp, QObject *parent) : QThread(parent) // 调用基类构造函数 { m_tcp = tcp; // 保存套接字指针 // 注意:套接字由外部创建,需确保外部生命周期管理正确 } // ============================================================ // ★ 线程执行函数(在子线程中运行) // ============================================================ void RecvFile::run() { qDebug() << "接收线程:" << QThread::currentThread(); // 打印当前线程ID,用于调试确认在子线程中 // ---------- 【创建临时文件】 ---------- QFile *file = new QFile("recv.txt"); // 先使用临时文件名 file->open(QFile::WriteOnly); // 以只写模式打开 // ---------- 【定义状态变量(lambda捕获)】 ---------- QString fileName; // 真实的文件名 qint64 totalSize = 0; // 文件总大小 qint64 recvSize = 0; // 已接收大小 bool headerReceived = false; // 是否已解析头部 // ============================================================ // ★ 连接readyRead信号(有数据到达时触发) // ============================================================ connect(m_tcp, &QTcpSocket::readyRead, this, [=]() mutable { // mutable允许修改按值捕获的变量 // 注意:lambda表达式在子线程中执行(信号所在线程) QByteArray data = m_tcp->readAll(); // 读取所有可用数据 // ---------- 【首次接收:解析头部】 ---------- if (!headerReceived) // 还没解析完头部 { int newlineIndex = data.indexOf('\n'); // 查找换行符位置 if (newlineIndex != -1) // 找到了文件名分隔符 { // 解析文件名 fileName = QString::fromUtf8(data.left(newlineIndex)); data = data.mid(newlineIndex + 1); // 剩余数据 if (data.size() >= 8) // 至少有8字节的大小数据 { // 拷贝8字节到totalSize memcpy(&totalSize, data.constData(), 8); data = data.mid(8); // 剩余数据(文件内容) headerReceived = true; // 头部解析完成 // ★ 用真实文件名重新创建文件 file->close(); file->deleteLater(); // 延迟删除旧文件对象 file = new QFile(fileName); file->open(QFile::WriteOnly); qDebug() << "接收文件:" << fileName << " 大小:" << totalSize; } } } // ---------- 【写入文件内容】 ---------- if (!data.isEmpty()) // 如果有数据 { file->write(data); // 写入文件 recvSize += data.size(); // 累加已接收大小 } // ---------- 【判断是否接收完毕】 ---------- if (headerReceived && totalSize > 0 && recvSize >= totalSize) { qDebug() << "文件接收完成!"; // 关闭并清理资源 m_tcp->close(); m_tcp->deleteLater(); file->close(); file->deleteLater(); emit over(); // 发射完成信号 } }); exec(); // ★ 进入事件循环,等待信号触发 // 如果不调用exec(),线程会立即退出,无法响应readyRead信号 }mainwindow.h
#ifndef MAINWINDOW_H // 防止头文件被重复包含的预处理指令 #define MAINWINDOW_H // 定义宏MAINWINDOW_H #include <QMainWindow> // 包含QMainWindow类头文件,主窗口基类 #include <QTcpServer> // 包含QTcpServer类头文件,用于TCP服务器端 QT_BEGIN_NAMESPACE // 开始Qt命名空间宏 namespace Ui { // 声明Ui命名空间 class MainWindow; // 前向声明UI类(由Qt Designer生成的界面类) } QT_END_NAMESPACE // 结束Qt命名空间宏 // MainWindow类继承自QMainWindow,是应用程序的主窗口 class MainWindow : public QMainWindow { Q_OBJECT // Qt宏,启用信号与槽机制 public: // 构造函数,parent参数默认为nullptr explicit MainWindow(QWidget *parent = nullptr); // 析构函数,用于清理资源 ~MainWindow() override; private slots: // 私有槽函数,响应"设置监听"按钮的点击事件 // 用于启动TCP服务器监听 void on_setListen_clicked(); private: Ui::MainWindow *ui; // 指向UI对象的指针,用于访问界面上的控件 QTcpServer* m_s; // 私有成员变量,指向TCP服务器对象的指针 }; #endif // MAINWINDOW_H // 结束条件编译指令完整执行流程
【发送端(客户端)】 【接收端(服务器)】 │ │ │ 程序启动 │ 程序启动 │ 创建 QThread (t) │ 创建 QTcpServer (m_s) │ 创建 SendFile (worker) │ │ worker->moveToThread(t) │ │ 连接所有信号槽 │ │ t->start() │ │ 等待用户操作 │ 等待用户操作阶段2:建立连接
【发送端】 【接收端】 │ │ │ 点击"连接服务器" │ 点击"监听" │ emit startConnect(8989, 127.0.0.1) │ listen(Any, 8989) │ │ │ connectServer 在子线程执行 │ │ connectToHost ──────────────────────→│ newConnection 触发 │ │ nextPendingConnection │ │ 创建 RecvFile 线程 │ │ subThread->start() │ │ 进入 run() + exec() │ 连接成功 │ │ connected 信号 → connectOK │ │ 主线程弹窗"连接成功" │阶段3:发送文件
【发送端】 【接收端】 │ │ │ 点击"选择文件" │ │ QFileDialog 选 D:\test.txt │ │ │ │ 点击"发送文件" │ │ emit sendFile(path) │ │ │ │ sendFile 在子线程执行 │ │ │ │ ① write("test.txt\n") ─────────────→│ ① 解析文件名 │ ② write(size, 8) ──────────────────→│ ② 解析大小 │ ③ write(内容64KB/次) ──────────────→│ ③ 写文件 │ │ │ 每发一块:emit curPercent(percent) │ │ 进度条更新 │ │ │ │ 发送完成 │ 接收完毕 │ │ emit over() │ │ 主线程弹窗"接收完毕"完整启动流程
阶段1:接收端启动(服务器)
【步骤1】启动接收端程序 ↓ 【步骤2】MainWindow构造函数执行 ├─ 创建UI界面 ├─ 创建QTcpServer对象 (m_s) └─ 连接newConnection信号 ↓ 【步骤3】用户点击"监听"按钮 ├─ 获取端口号 (如8989) ├─ 调用 m_s->listen(QHostAddress::Any, port) └─ 服务器开始监听,等待客户端连接 ↓ 【步骤4】服务器进入等待状态 └─ 主线程事件循环运行代码对应:
// mainwindow.cpp (接收端) MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); m_s = new QTcpServer(this); // 创建服务器 // 监听新连接信号 connect(m_s, &QTcpServer::newConnection, this, [=](){ // 当有客户端连接时执行... }); } void MainWindow::on_setListen_clicked() { unsigned short port = ui->port->text().toUShort(); m_s->listen(QHostAddress::Any, port); // 开始监听 }阶段2:发送端启动(客户端)
【步骤1】启动发送端程序 ↓ 【步骤2】MainWindow构造函数执行 ├─ 创建UI界面 ├─ 创建QThread对象 (子线程) ├─ 创建SendFile对象 (任务对象) ├─ worker->moveToThread(t) // 移动到子线程 ├─ 连接各种信号槽 └─ t->start() // 启动子线程 ↓ 【步骤3】子线程进入事件循环 └─ 等待接收信号代码对应:
// mainwindow.cpp (发送端) MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 创建子线程 QThread* t = new QThread; SendFile* worker = new SendFile; worker->moveToThread(t); // ★ 核心:移动到子线程 // 连接信号 connect(this, &MainWindow::sendFile, worker, &SendFile::sendFile); connect(this, &MainWindow::startConnect, worker, &SendFile::connectServer); connect(worker, &SendFile::connectOK, this, [=](){ QMessageBox::information(this, "连接服务器", "连接成功!"); }); connect(worker, &SendFile::curPercent, ui->progressBar, &QProgressBar::setValue); t->start(); // 启动子线程 }阶段3:建立TCP连接
【步骤1】发送端用户点击"连接服务器" ↓ 【步骤2】主线程发射 startConnect(port, ip) 信号 ↓ 【步骤3】子线程中的 SendFile::connectServer() 被调用 ├─ 清理旧连接 (如果有) ├─ 创建 QTcpSocket 对象 ├─ m_tcp->connectToHost(ip, port) // 发起连接 └─ 连接信号槽 (connected, disconnected) ↓ 【步骤4】接收端收到连接请求 ├─ QTcpServer 触发 newConnection() 信号 ├─ QTcpSocket *tcp = m_s->nextPendingConnection() ├─ 创建 RecvFile* subThread = new RecvFile(tcp) └─ subThread->start() // 启动接收线程 ↓ 【步骤5】接收端接收线程启动 ├─ RecvFile::run() 在新线程中执行 ├─ 创建临时文件 "recv.txt" ├─ 连接 m_tcp 的 readyRead 信号 └─ exec() 进入事件循环 ↓ 【步骤6】发送端连接成功 └─ 发射 connectOK() 信号 → 主线程弹出"连接成功"对话框流程图:
发送端(主线程) 发送端(子线程) 接收端(主线程) 接收端(子线程) │ │ │ │ │ 点击连接按钮 │ │ │ │─────────────────────────►│ │ │ │ │ connectToHost() │ │ │ │─────────────────────────►│ │ │ │ │ newConnection() │ │ │ │─────────────────────────►│ │ │ │ 创建RecvFile │ │ │ 启动线程 │ │ │ │ │ │ connected信号 │ │ │ │◄─────────────────────────│ │ │ │ │ │ │ connectOK信号 │ │ │ │◄─────────────────────────│ │ │ │ │ │ │ │ 弹窗"连接成功" │ │ │阶段4:文件传输流程
【步骤1】发送端用户点击"选择文件" ├─ 打开文件对话框 ├─ 选择文件 └─ 显示文件路径 ↓ 【步骤2】发送端用户点击"发送文件" ├─ 主线程发射 sendFile(path) 信号 └─ 子线程中的 SendFile::sendFile() 被调用 ↓ 【步骤3】发送端子线程执行 sendFile() ├─ 检查连接状态 ├─ 打开文件 (只读模式) ├─ 获取文件信息 (文件名、大小) │ ├─ 【发送协议头部】 │ ├─ 发送文件名 + "\n" │ └─ 发送文件大小 (8字节) │ ├─ 【分块发送文件内容】 │ ├─ 循环读取 64KB 数据块 │ ├─ 发送数据块 │ ├─ 计算进度 │ └─ 发射 curPercent(进度) 信号 │ └─ 关闭文件 ↓ 【步骤4】接收端子线程接收数据 ├─ readyRead 信号触发 ├─ 读取所有数据 │ ├─ 【首次接收:解析头部】 │ ├─ 查找 '\n' 获取文件名 │ ├─ 读取8字节获取文件大小 │ └─ 创建真实文件 │ ├─ 【持续接收:写入文件】 │ ├─ 写入数据到文件 │ └─ 累加已接收大小 │ └─ 【接收完成】 ├─ 关闭文件 ├─ 关闭套接字 └─ 发射 over() 信号 ↓ 【步骤5】接收端主线程收到完成信号 ├─ subThread->exit() // 退出事件循环 ├─ subThread->wait() // 等待线程结束 ├─ subThread->deleteLater() // 删除线程 └─ 弹出"文件接收完毕!"对话框数据传输协议:
┌──────────────────────────────────────────────────────────┐ │ 数据包格式 │ ├──────────────────────────────────────────────────────────┤ │ [文件名] [\n] [文件大小8字节] [文件内容...] │ │ │ │ 示例: │ │ "test.jpg\n" → 文件名 │ │ 0x00 0x00... → 文件大小 (如 102400) │ │ [图片数据...] → 文件内容 │ └──────────────────────────────────────────────────────────┘线程交互时序图:
发送端子线程 发送端主线程 接收端子线程 接收端主线程 │ │ │ │ │ 发送文件名+大小 │ │ │ │─────────────────────────►│─────────────────────────►│ │ │ │ │ readyRead触发 │ │ │ │ 解析头部 │ │ │ │ │ │ 发送64KB数据块 │ │ │ │─────────────────────────►│─────────────────────────►│ │ │ │ │ 写入文件 │ │ curPercent(进度) │ │ │ │─────────────────────────►│ │ │ │ │ 更新进度条 │ │ │ │ │ │ │ 发送下一个数据块 │ │ │ │─────────────────────────►│─────────────────────────►│ │ │ │ │ 继续写入 │ │ │ │ │ │ ... 重复直到发送完毕 │ │ │ │ │ │ │ │ │ │ 接收完成 │ │ │ │ over()信号 │ │ │ │─────────────────────────►│ │ │ │ │ 清理资源 │ │ │ │ 弹窗提示关键流程详解
信号槽跨线程调用流程
// 主线程 → 子线程 emit sendFile(path); // 主线程发射 // ↓ 内部机制 // 1. 信号参数打包到事件中 // 2. 放入子线程的事件队列 // 3. 子线程事件循环取出事件 // 4. 调用槽函数 sendFile(path) 在子线程执行 // 子线程 → 主线程 emit curPercent(50); // 子线程发射 // ↓ 内部机制 // 1. 信号参数打包 // 2. 放入主线程的事件队列 // 3. 主线程事件循环取出 // 4. 调用槽函数 setValue(50) 在主线程执行TCP连接建立详细过程
1. 发送端调用 connectToHost() └─ 操作系统发起三次握手 2. 接收端操作系统收到SYN包 └─ QTcpServer 内部检测到新连接 └─ 触发 newConnection() 信号 3. 三次握手完成 └─ 发送端套接字状态变为 ConnectedState └─ 触发 connected() 信号文件接收状态机
[初始状态] ↓ 等待接收头部 ↓ (收到文件名和大小) [头部已接收] ↓ 接收文件内容 ↓ (recvSize >= totalSize) [接收完成] ↓ 发送 over 信号 → 清理资源异常情况处理流程
连接失败
发送端连接失败 ├─ connectToHost() 超时 ├─ 触发 error() 信号 └─ 需要手动重连或提示用户传输中断
网络中断 ├─ 发送端: 检测到 disconnected 信号 │ ├─ 关闭套接字 │ ├─ 发射 gameover() 信号 │ └─ 主线程清理子线程资源 │ └─ 接收端: 检测到套接字断开 ├─ 关闭文件 ├─ 清理资源 └─ 提示用户传输失败文件打开失败
发送端打开文件失败 ├─ 打印错误日志 └─ 直接返回,不发送 接收端创建文件失败 ├─ 文件权限不足 ├─ 磁盘空间不足 └─ 可能需要提示错误资源清理流程
发送端清理
用户关闭程序 或 网络断开 ↓ 发送端主线程收到 gameover() 信号 ├─ t->quit() // 退出子线程事件循环 ├─ t->wait() // 等待子线程结束 ├─ t->deleteLater() // 延迟删除线程对象 └─ worker->deleteLater() // 延迟删除任务对象接收端清理
文件接收完成 或 连接断开 ↓ 接收端子线程 over() 信号 ├─ subThread->exit() // 退出事件循环 ├─ subThread->wait() // 等待线程结束 ├─ subThread->deleteLater() // 删除线程 └─ 弹窗提示用户完整运行示例
示例场景:发送 "test.jpg" 文件
1. 接收端启动,监听 8989 端口 2. 发送端启动,输入 IP:127.0.0.1 端口:8989 3. 发送端点击"连接服务器" → 建立连接,弹出"连接成功" 4. 接收端有客户端连接 → 创建接收线程,等待接收数据 5. 发送端点击"选择文件",选择 test.jpg (1MB) 6. 发送端点击"发送文件" → 发送协议: "test.jpg\n" + 文件大小(8字节) + 数据块... 7. 接收端接收数据 → 解析头部,创建 test.jpg → 持续写入数据 → 进度实时更新 (虽然接收端没显示) 8. 发送端进度条 0% → 100% 9. 接收端接收完毕,弹出"文件接收完毕!" 10. 传输完成
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐










































































































所有评论(0)