在标准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为false

1.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 服务器对比

对比项 客户端 服务器
核心类 QTcpSocket QTcpServer + QTcpSocket
启动方式 connectToHost(ip, port) listen(Any, port)
连接成功信号 connected newConnection
获取连接 nextPendingConnection()
收数据 readyRead + readAll() readyRead + readAll()
发数据 write() write()
断开 close() / disconnected disconnected
重连策略 ✅ 复用 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 obj obj->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. 传输完成

Logo

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

更多推荐