• 03 - Qt 多线程网络通信——套接字


    文章序言

    1. 日志1.0 :在QT中如何使用TCP协议进行套接字通信(即指网络通信)
    2. TCP 和 UDP是传输层协议, 二者的区别:
      • TCP是面向连接的流式传输协议;TCP传输, 数据安全;
      • UDP是面向无连接的报式传输协议;UDP的传输, 数据不安全;【比如, 抖音刷视频, 还有直播等等】
    3. 面向连接:TCP在连接的时候, 需要进行3次握手;断开的时候, 需要进行4次挥手;即TCP是一种双向连接, 双向断开的机制;
    4. UDP:在通信之前是不需要准备的, 直接通信不需要连接, 所以在UDP中是没有3次握手与4次挥手的;
    5. TCP在进行数据传输的时候, 有数据校验机制, 当数据包丢失之后, 会自动进行重传;而UDP中是没有数据校验机制的, 数据丢失就无法找回;
    6. 因为TCP流式传输协议, 所以, 发送端和接收端处理的数据量可以不均等, 例如, 在发送端发送10M的数据, 而在接收端可以分10次, 每次抽1M;
    7. UDP是报文形式的, 在UDP传输数据的时候, 是以报文的形式发送的, 那么一个报文有多长, 在接收端就收多长, 例如:发送50M的数据包过去, 但是在接收端收不到50M, 就会把整个包都丢了, 不存在先收一部分数据的情况;

    一、 Qt套接字通信概述

    1. TCP和UDP的通信流程

    1.1. TCP的通信流程

    请添加图片描述

    1.2. UDP的通信流程

    请添加图片描述

    无论是TCP还是UDP通信, 其通信流程都是一样的, 只是使用的语言可能不一样, 而后面要用的QT, 也只是在QT中对通信流程进行封装, 让操作更加简单, 其实底层的通信原理和通信的过程完全相同的;

    如果要进行网络通信, 它和语言是没有任何关系的, 要进行网络通信, 只需要关注到底是基于TCP还是UDP(两个传输层的协议)通信;
    在传输层协议定下来后, 往上还有应用层(FTP协议,HTTP协议)

    1.3. QT 网络通信用到的两个类

    1. 使用QT提供的类进行基于TCP的套接字通信需要用到两个类:
    • QTcpServer:服务器端使用, 服务器类, 用于监听客户端连接以及和客户端建立连接;
    • QTcpSocket:服务器端和客户端都要用到的, 进行客户端和服务器端的通信, 接收和发送数据;通信的套接字类。
    1. 在QT中进行网络通信, 其实也要进行I/O操作, 这里的I/O是指网络I/O, 而不是磁盘I/O, 操作的是网络数据;
    2. 在QT中, 用于套接字通信的QTcpSocket类(网络数据的接收发送), 和QT中进行文件操作的QFile类(文件的读写), 其祖先类是一样的,即QIODevice;二者本质一样, 都是进行I/O操作, 只不过操作的对象不一样。

    二、QTcpServer类的常用API

    负责监听, 并且接受客户端的连接;

    1. 查阅QT帮助文档, 可以看到QTcpServer类属于network模块, 所以, 在.pro工程文件中, 需要把这个模块加上编译
      QTcpServer

    1. Public Functions —— 公共成员函数

    1.1. 构造函数:构造一个用于监听的服务器端对象

    QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR)
    
    • 构造一个用于监听的服务器端对象, 这个构造函数接收一个参数, QObject *parent = Q_NULLPTR, 即指定一个父对象
    • 父对象这个概念很有用!(可以了解一下QT的对象树概念)在QT中存在一个内存回收机制, 通过指定父对象, 就可以将当前的节点挂在父对象的下面, 当父对象析构的时候, 会自动析构其子节点, 这就可以保证将挂在这个父对象下的所有子节点都析构掉(树状结构, 操作时递归完成);
    • 父对象和父类不是一个概念;父类是有继承关系的, 而父对象没有

    1.2. 给监听的套接字设置监听:listen

    • 设置监听 -》 对应前面TCP通信流程中的服务器的bind和listen两步(先绑定再设置监听)
    // 设置监听 -》 对应前面TCP通信流程中的服务器的bind和listen两步(先绑定再设置监听)
    bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
    
    • 参数1:const QHostAddress &address = QHostAddress::Any, 要绑定的本地地址, Any指绑定本地任意的IP地址, 可以使用这个默认参数, 这个Any支持IPV4也支持IPV6;
    • 参数2:对应主机上的对应端口(会为某个进程绑定固定的端口)
    • 确定了IP和端口, 数据就可以发送到某台主机上的某个进程上(确定主机的应用程序, 端口就是用来确定应用程序的 | 而IP用来定位主机);
    • port = 0, 代表服务器随机分配一个端口(开发者就不知道了), 所以, 最好自己传入固定的端口号;端口号:0 - 65535;最好指定5000(8000, 10000以上)以上的端口号, 5000以下有些可能会被操作系统占用;
    • QHostAddress类封装IPV4, IPV6
      • 在这里插入图片描述

    1.3. 判断当前的服务器对象是否已经开始监听了:isListen

    bool QTcpServer::isListening() const
    // 若当前的服务器对象已经开始监听就返回true, 否则false;
    // 只有服务器端开始监听, 才知道有客户端连接, 并且和客户端建立连接
    

    1.4. 服务器地址和端口信息

    • QHostAddress QTcpServer::serverAddress() const : 如果当前服务器对象正在监听, 则返回监听的服务器地址信息, 否则返回QHostAddress::Null
    • quint16 QTcpServer::serverPort() const:如果当前服务器对象正在监听连接, 则返回服务器的端口, 否则返回0;

    1.5. nextPendingConnection() : 返回下一个挂起的连接作为已连接的QTcpSocket对象。

    • [virtual] QTcpSocket *QTcpServer::nextPendingConnection():当服务器启动监听之后, 若有客户端连接上来, 那么服务器就会与客户端建立连接, 建立连接之后就会有一个用于通信的套接字对象
    • 这里注意一下, 虽然返回的是一个指针, 但是地址在函数体内部分配出来, 所以这里指针指向一块有效的堆内存;所以, 用完后要释放, 你可以自己释放掉,也可以让服务器对象自己释放:前面讲过了对象树的概念, 这里的QTcpSocket对象是由QTcpServer(父对象)对象返回的, 二者是父子对象关系, 这就组成了一个对象树, 当QTcpServer对象析构的时候会先析构QTcpSocket对象(结构上的父子关系 ),保证了内存不泄漏,但是这两者是没有继承关系的;

    1.6. 等待新连接:waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR)

    • bool QTcpServer::waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR):这个函数是一个阻塞函数, 当服务器端启动监听之后, 调用这个函数会阻塞当前的服务器线程, 阻塞线程等待客户端的连接, 若没有客户端来连接, 就会一直阻塞住, 所以可以指定阻塞时长msec(毫秒), 当时间到了还没有客户端连接, 函数就会解除阻塞;
    • 而第二个参数timedOut:用来标识是超时解除的阻塞, 还是非超时解除的阻塞;
    1. msec:指定阻塞的最大时长, 单位为毫秒(ms);
    2. timedOut:传出参数, 如果操作超时timedOut为true, 没有超时为false;(这里是要传入地址的, 因为是bool *)
    3. 因为这是一个阻塞函数, 所以在开发的时候, 不推荐使用它来检测有没有新的客户端来连接, 而是使用信号和槽的方式, 这样就不会阻塞了;

    2. 信号

    2.1. [signal] void QTcpServer::newConnection()

    • 当有新的客户端连接上来, 这个类就会发出newConnection()信号, 然后connect绑定槽函数, 在槽函数中(处理对应的新连接)调用nextPendingConnection(), 得到一个用于通信的套接字对象, 基于这个对象去接收和发送数据;

    2.2. [signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError)

    • 当服务器和客户端建立连接的时候失败了, 就会发出这样的信号, 通过这个信号传递出的参数, 就可以知道和客户端建立连接失败的原因。

    三、QTcpSocket类的常用API

    • 用于通信的套接字类:QTcpSocket, 关于这个类的使用有两种情况:
      • case 1: 在服务器端调用nextpendingConnection(), 得到直接用于通信的QTcpSocket;
      • case 2: 在客户端创建QTcpSocket对象, 创建出来的这个对象不能直接用来通信, 需要先连接服务器, 连接成功之后才能和服务器进行通信;

    QTcpSocket如何连接服务器;

    connectToHost:连接服务器

    • 这个函数是重载函数;
      • [virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol)
        • 参数1 : 指定要连接的服务器的地址, IP地址;
        • 参数2 : 端口, 是服务器端程序绑定的端口, 服务器绑定了什么端口, 在连接的时候就基于什么端口连接;
        • 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
        • 参数4 : 用默认值, IP协议;
      • [virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite)
        • 参数1 :指定要连接的服务器地址, 通过QHostAddress进行封装;
        • 参数2 : 端口, 是服务器端程序绑定的端口
        • 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?

    1. 公共成员函数

    如何基于QTcpSocket进行通信, 通信即是指数据的读或写;

    • QTcpSocket是一个套接字通信类, 无论是在客户端还是服务器端都需要使用它。在Qt中发送和接收数据也属于I/O操作(网络I/O), 如下是这个类的继承关系:
      • 在这里插入图片描述

    1.1. 构造函数

    • QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR); : 创建一个状态为UnconnectedState的QTcpSocket对象。

    1.2. 连接服务器:connectToHost

    需要指定服务器绑定的IP和端口信息
    继承自QAbstractSocket

    • 这个函数是重载函数;
      • [virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol)
        • 参数1 : 指定要连接的服务器的地址, IP地址;
        • 参数2 : 端口, 是服务器端程序绑定的端口, 服务器绑定了什么端口, 在连接的时候就基于什么端口连接;
        • 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
        • 参数4 : 用默认值, IP协议;
      • [virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite)
        • 参数1 :指定要连接的服务器地址, 通过QHostAddress进行封装;
        • 参数2 : 端口, 是服务器端程序绑定的端口
        • 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?

    1.3. 接收数据:相关的read

    在 Qt 中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由 Qt 框架维护的一块内存。所以,调用了发送函数,数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据。

    • qint64 QIODevice::read(char *data, qint64 maxSize); : 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中;
    • QByteArray QIODevice::read(qint64 maxSize);:指定可接收的最大字节数 maxSize,返回接收的字符串;
    • QByteArray QIODevice::readAll();:将当前可用操作数据全部读出,通过返回值返回读出的字符串;

    1.4. 发送数据:相关的write

    • qint64 QIODevice::write(const char *data, qint64 maxSize);发送指针 data 指向的内存中的 maxSize 个字节的数据

    • qint64 QIODevice::write(const char *data);: 发送指针 data 指向的内存中的数据,字符串以 ‘\0’ 作为结束标记

    • qint64 QIODevice::write(const QByteArray &byteArray);:发送参数指定的字符串

    • 调用read和write操作的都是QT帮我们维护的这块内存, QT框架检测到有数据后, 会把数据发送到网络中,所以read和write不是直接操作的网络中的数据;

    2. 信号

    查阅帮助文档, 可以发现QTcpSocket所用到的信号都是从父类QAbstractSocket继承来的;

    2.1. QAbstractSocket中的信号:

    在这里插入图片描述

    2.1.1. [signal] void QAbstractSocket::connected()
    • This signal is emitted after connectToHost() has been called and a connection has been successfully established:当我们调用connectToHost()时, 如果成功连接了服务器, 那么这个信号就会被发射出来;
    2.1.2. [signal] void QAbstractSocket::disconnected()
    • This signal is emitted when the socket has been disconnected:在套接字断开连接时发出 disconnected() 信号;
    • 若B端断开了连接, 在A端通信的套接字对象(QAbstractSocket | QTcpSocket)就会发射出disconnected信号;(检测对端有没有断开连接, 不是当前这端, 而是通信的对端

    2.2. QIODevice中的信号:

    2.2.1. [signal] void QIODevice::readyRead()
    • 在使用QTcpSocket进行套接字通信的过程中, 如果该类对象发射出readyRead()信号, 说明对端发送的数据达到了, 之后就可以调用read函数接收数据了;

    四、QT中基于TCP进行套接字通信的流程

    1. 服务器端

    1.1 通信流程

    1. 创建套接字服务器QTcpServer对象(用于监听的套接字对象
    2. 绑定 + 设置监听:通过QTcpServer对象设置监听, 即:QTcpServer::listen() : IP + 端口
    3. 基于QTcpServer::newConnection()信号检测是否有新的客户端连接;
    4. 如果发出了newConnection()的信号, 说明有新的客户端连接, 就在newConnection对应的槽函数中调用 [virtual] QTcpSocket *QTcpServer::nextPendingConnection()得到通信的套接字对象;
    5. 使用通信的套接字对象QTcpSocket和客户端进行通信:QTcpSocket中提供了接收和发送数据的函数read和write;并且在QTcpSocket中提供了3个信号:
      • QIODevice::readyRead():可以知道对端有没有发送数据过来, 若有数据过来, QTcpSocket对象会发出readyRead信号,在这个信号对应的槽函数中做对应的接收数据的操作read即可;
      • 还可以通过QTcpSocket对象去检测当前的连接是否成功了:[signal] void QAbstractSocket::connected(),这个连接的检测是在客户端进行检测的, 服务器端是不可以使用这个信号的;
      • [signal] void QAbstractSocket::disconnected():无论在客户端或者服务器端都是可以使用的, 只要通信的两端(A, B), 其中的某一端(B)断开了连接, 那么在这一端(A)就可以通过TcpSocket对象发射出这个信号, 通知A端, 对端B已经断开了连接;

    1.2 服务器窗口界面设计:

    1. 创建一个服务器的工程, base 基类选择QMainWindow, 这样会带菜单栏, 工具栏和状态栏;
      在这里插入图片描述
    2. 菜单界面设计:详细图解
      在这里插入图片描述

    1.3 服务器端套接字通信处理

    开始开发之前, 先在.pro工程文件中将network模块加入进去;记得保存刷新, 才能将模块重新导入;
    在这里插入图片描述
    另外, 在Qt Creator中, 提供了可以在UI设计界面, 直接右键控件转到槽(如下图所示:), 由系统自动帮我们去进行信号和槽的连接, 这里为了加深练习, 全部都统一使用手动连接信号和槽:
    在这里插入图片描述

    1. 按照前面的流程, 先在服务器端创建一个用于监听的套接字对象, QTcpServer
    2. 将状态栏的两种连接状态图片(连接成功和连接失败)添加到项目的资源文件中;

    1.4 完整服务器端代码

    1. mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    #include 
    #include 
    #include 
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private:
        Ui::MainWindow *ui;
        QTcpServer * m_Server;
        QTcpSocket * m_tcp;
    
        QLabel * m_status;  // 将状态图片放到QLabel中
    
        bool isCon;
    };
    #endif // MAINWINDOW_H
    
    1. mainwindow.c
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include 
    // QT 中的connect执行完之后, 不代表对应的槽函数就执行了, QT中的connect是先进行了注册, 告诉QT框架, 假设有对应的事件发生了, 某个信号就会被发射出来
    // 信号被发射出来, 某个对象就会接收到发射出来的信号, 且调用连接的槽函数来处理这个信号;
    // 其实这个connect操作就是注册了一个回调, 当实际传输之后, 回调函数即槽函数才能被调用;
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        isCon = false;
        setWindowTitle("服务器端");
        // 1. 创建监听的服务器对象, 将其实例化
        // 这里可以指定父对象, 这样在当前MainWindow对象析构后, 会自动析构QTcpServer类型的对象(对象树)
        m_Server = new QTcpServer(this);
    
        // 先设置一个端口号, 方便测试
        ui->port->setText("8899");
        ui->btn_Close->setDisabled(true);   // 当还没有启动监听的时候, 关闭监听的按钮就是不可用的
    
    
        // 2. 点击按钮启动监听 | 槽函数用的是匿名函数的写法
        connect(ui->btn_setListen, &QPushButton::clicked, this, [=](){
            // 得到端口号, 转成无符号短整形
            unsigned short port = ui->port->text().toUShort();
    
            // 设置监听, 绑定本机任意IP地址, 还有port端口值
            m_Server->listen(QHostAddress::Any, port);
    
            // 当启动监听之后, 就让按钮变成不可用状态
            ui->btn_setListen->setDisabled(true);
    
            ui->btn_Close->setDisabled(false);  // 关闭监听的按钮设置为可用的
    
        });
    
        // 3. 等待客户端的连接
        // 当有客户端连接到来之后, QTcpServer对象就会发出一个newConnection信号(当有新连接建立, 就要修改一下连接状态为成功), 在槽函数中得到用于通信的QTcpSocket对象
        connect(m_Server, &QTcpServer::newConnection, this, [=](){
           // 通过得到的这个实例对象进行通信 | 定位到类名上, alt + 回车键可以帮我们自动补全头文件
             m_tcp = m_Server->nextPendingConnection();
             ui->record->append("已经成功和客户端进行连接......");
             // 修改连接状态
             m_status->setPixmap(QPixmap(":/success.png").scaled(20, 20));
             isCon = true;
            // 实例对象, 什么时候可以接收数据?
            // 检测是否可以接收数据 | 当tcp对象发射出readyRead信号, 就可以接收数据了
            // 在槽函数中进行数据的接收
            connect(m_tcp, &QTcpSocket::readyRead, this, [=](){
               // readAll 接收所有数据 | 读到客户端发来的所有数据, 放到历史记录框中
                QByteArray data = m_tcp->readAll();
                ui->record->append("客户端 say: " + data);
    
            });
    
            // 一切由QT框架维护
            // 当QTcpSocket对象发出disconnected信号, 说明对端已经断开了连接
    
            connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
                // 如果客户端和当前的服务器断开了连接 , 那么服务器也需要断开连接, 双向断开
                // 关闭套接字 | 接着要去释放QTcpServer(m_tcp)指针指向的内存
                m_tcp->close();
                m_tcp->deleteLater();   // 里面封装了delete, 或者直接 delete m_tcp
                // 修改状态栏
                m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
                isCon = false;
                ui->record->append("服务器端已经和客户端断开了连接");
            });
    
    
        });
        connect(ui->btn_Close, &QPushButton::clicked, this, [=](){
                m_tcp->close();
                ui->btn_Close->setDisabled(true);   // 设置关闭监听的按钮为不可用;
                ui->btn_setListen->setEnabled(true);
    //                ui->record->append("服务器端已经和客户端断开了连接");
            });
    
        // 4. 点击发送信息按钮, 发送信息给客户端
        connect(ui->sendMsg, &QPushButton::clicked, this, [=](){
            if (isCon)
            {
                QString msg = ui->msg->toPlainText();   // 把数据以文本形式都取出来
                // 取出数据后要发送给客户端 | msg.toUtf8()将QString类型的转换成QByteArray类型
                m_tcp->write(msg.toUtf8());
    
                // 将发送出去的信息保存到历史记录文本框中
                ui->record->append("服务器端 say:" + msg);
            }
            else
            {
                // 如果没有连接, 发送消息要提示
                QMessageBox::critical(this, "错误提示", "没有连接上客户端, 发送消息失败");
            }
    
        });
    
        // 5. 状态栏处理连接状态
        // 实例化QLabel对象
        m_status = new QLabel;
    
        // 默认情况下未连接状态 | 因为存入的图片可能很大, 所以还要指定缩放
        m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
        m_status->setScaledContents(true);  // 控件自适应图片大小
    
        ui->statusbar->addWidget(new QLabel("连接状态:"));
        // 将这个控件m_status添加到状态栏中
        ui->statusbar->addWidget(m_status);
    
    
    
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    // 存在一个BUG 为什么有客户端先断开的连接, 服务器端再关闭监听, 就会闪退
    

    2. 客户端

    2.1 通信流程

    1. 创建用于通信的套接字类QTcpSocket对象:实例化后要去连接服务器, 否则无法进行套接字通信;
    2. 使用服务器端绑定的IP和端口连接服务器:QAbstractSocket::connectToHost();
    3. 连接成功之后, 就可以使用QTcpSocket对象和服务器进行通信;

    2.2 客户端窗口界面设计

    1. 创建一个客户端的工程, base 基类选择QMainWindow, 这样会带菜单栏, 工具栏和状态栏;
      在这里插入图片描述
    2. 菜单界面设计:详细图解
      在这里插入图片描述

    2.3 客户端套接字通信处理代码

    按照通信流程走;

    1. mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    #include 
    #include 
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private:
        Ui::MainWindow *ui;
        QTcpSocket * m_tcp;
        QLabel * m_status;
        bool isConnect; // 标志位判断有没有断开连接
    };
    #endif // MAINWINDOW_H
    
    1. mainwindow.c
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include 
    #include 
    // 连接按钮和断开连接按钮互斥使用;
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        isConnect = false;
        // 设置默认的IP(本地回环地址)和端口地址
        ui->port->setText("8899");
        ui->IP->setText("127.0.0.1");
        setWindowTitle("客户端");
        // 1. 创建用于通信的套接字对象
        m_tcp = new QTcpSocket(this);
        ui->btn_Disconnect->setDisabled(true);
    
        // 2. 连接服务器按钮
        connect(ui->btn_Connect, &QPushButton::clicked, this, [=](){
            QString ip = ui->IP->text();
            unsigned short port = ui->port->text().toUShort();
    
            // 连接服务器
            m_tcp->connectToHost(QHostAddress(ip), port);
        });
    
        // 断开连接按钮
        connect(ui->btn_Disconnect, &QPushButton::clicked, this, [=](){
            m_tcp->close(); // 可以这里主动断开连接
            isConnect = false;
            ui->btn_Connect->setDisabled(false);
            ui->btn_Disconnect->setDisabled(true);
        });
    
        // 3. 发送信息按钮
        connect(ui->btn_SendMsg, &QPushButton::clicked, this, [=](){
            if (isConnect)
            {
                QString msg = ui->sendMsg->toPlainText();   // 拿到发送信息框的内容
    
                // 发送
                m_tcp->write(msg.toUtf8());
                // 将发送出去的信息放到历史记录框中
                ui->histroyEdit->append("客户端 say:" + msg);
            }
            else
            {
                QMessageBox::critical(this, "错误提示", "两端断开连接, 消息发送不成功");
            }
        });
    
    
    
        // 4. 当用于通信的Tcpsocket套接字对象发送出了一个readyRead信号就说明有数据到来了(所以这是被动的)
        connect(m_tcp, &QTcpSocket::readyRead, this, [=](){
            QByteArray data = m_tcp->readAll();
            ui->histroyEdit->append("服务器端 say:" + data);
        });
    
        // 5. 当通信的时候, 怎么知道服务器已经断开连接了 | 当用于通信的套接字对象发射出一个disconnected信号
        connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
            m_tcp->close(); // 双向断开连接
            // 因为上面添加了父对象, 所以这里可以不用自己手动释放 | 而且在这里就释放了资源, 客户端断开连接,再次重连就会有问题
            // m_tcp->deleteLater();
            // 修改状态栏的状态
            m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
            ui->histroyEdit->append("服务器已经和客户端断开了连接 ");
    
            ui->btn_Connect->setDisabled(false);
            ui->btn_Disconnect->setDisabled(true);
            isConnect = false;
        });
        // 6. 当通信的套接字对象发射出connected信号, 就说明连接成功
        connect(m_tcp, &QTcpSocket::connected, this, [=](){
            m_status->setPixmap(QPixmap(":/success.png").scaled(20, 20));
            ui->histroyEdit->append("已经成功连接到了服务器......");
            ui->btn_Connect->setDisabled(true);
            ui->btn_Disconnect->setDisabled(false);
            isConnect = true;
        });
    
        // 状态栏初始化
        ui->statusbar->addWidget(new QLabel("连接状态:"));
        m_status = new QLabel(this);
        m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
        ui->statusbar->addWidget(m_status);
    
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    

    五、子线程中实现客户端传输文件给服务器端实例

    • 根据IP和端口连接服务器, 当点击发送文件按钮后, 会将选择的磁盘文件传输给服务器端;
    • 当服务器端接收完毕后, 服务器会断开连接, 客户端这边会发射出一个disconnected信号, 通知服务器已经接收完毕, 客户端断开连接;
    • 以上通信流程都是在子线程中完成;

    1. 客户端界面设计

    在这里插入图片描述
    在这里插入图片描述

    2. 客户端在子线程中连接服务器

    Qt中提供了两种线程的创建方式;步骤如下,我们使用其中的一种, 更为灵活的创建方式

    2.1 过程步骤

    1. 创建一个新的类SendFile, 让这个类从QObject派生
      在这里插入图片描述
    2. 在这个类中添加自定义的任务函数(公共的成员函数 | 子线程执行任务), 函数体就是我们要子线程中执行的业务逻辑
      在这里插入图片描述
    3. 主线程中创建一个QThread对象, 这就是子线程的对象;
    4. 主线程中创建工作的类对象, 并且不要给创建的对象指定父对象
    5. 将创建出来的工作的类对象移动到创建的子线程对象中, 需要调用QObject类提供的moveToThread()方法:
    -若前面创建的时候给work指定了父对象, 这里的移动就会失败
    work->moveToThread(subThread);	// 移动到子线程中工作;
    
    1. 启动子线程, 调用start(), 这时候线程启动了, 但是移动到线程中的对象是没有工作的;
    2. 借助信号和槽机制, 调用工作的类对象的工作函数, 让这个函数开始执行, 这时候是在移动到的那个子线程中运行的;

    2.2 实例代码

    1. mainwindow.cpp
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include "sendfile.h"
    
    #include 
    #include 
    #include 
    #include 
    #include 
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        ui->IP->setText("127.0.0.1");
        ui->port->setText("8899");
    
        // 对进度条初始化
        ui->ProgressBar_trans->setRange(0, 100);
        ui->ProgressBar_trans->setValue(0);
    
        // 都没有指定父对象, 后面要记得自己手动析构
        // 创建线程对象
        QThread * sub = new QThread;
    
        // 创建任务对象
        SendFile * worker = new SendFile;
    
        worker->moveToThread(sub);
    
        // 1. 连接服务器
        connect(ui->btn_Con, &QPushButton::clicked, this, [=](){
            QString ip = ui->IP->text();    // 获取IP值
            unsigned short port = ui->port->text().toUShort();  // 获取端口值, 并转换成无符号整型
    
            // 发送信号, 让worker里面的任务函数开始执行;
            // 在mainwindow中添加自定义的信号
            // 发射出信号后, 让某个对应的任务函数开始执行
            emit startConnect(port, ip);
    
        });
    
        // 若当前的窗口对象, 发射出一个startConnect信号,worker对象去接收这个信号, 调用类中的任务函数  | 信号和槽, 对应的参数要一致
        connect(this, &MainWindow::startConnect, worker, &SendFile::connectServer);
    
        // 工作对象接收信号
        connect(this, &MainWindow::sendFileSignal, worker, &SendFile::sendFileToServer);
    
    
        // 处理主线程发送的信号
        connect(worker, &SendFile::connectOK, this, [=](){
           QMessageBox::information(this, "连接服务器", "服务器和客户端连接成功");
        });
    
        // 处理主线程发送的信号 | 断开连接
        connect(worker, &SendFile::gameOver, this, [=](){
           QMessageBox::information(this, "连接服务器", "服务器和客户端断开连接");
    
           // 进行资源的释放
           sub->quit(); // 让线程退出
    
    //       调用quit()之后,将线程停止了,但是如果用户再次触发这个线程的启动,那么会导致你delete 了一个正在运行的线程,因此需要wait()来等待QThread子线程的结束
           sub->wait();
    
           // 析构worker对象
           worker->deleteLater();
           sub->deleteLater();  // 线程对象析构
        });
    
    
    
    
        // 2. 选择文件
        connect(ui->btn_selFile, &QPushButton::clicked, this, [=](){
            // 弹出选择文件对话框
            // 获得某个磁盘文件对应的绝对路径
            QString path = QFileDialog::getOpenFileName();
    
            // 容错处理, 若选择的路径是空的, 则停止后续操作
            if (path.isEmpty())
            {
                QMessageBox::warning(this, "打开文件", "选择的文件路径不能为空");
                return ;
            }
    
            // 将得到的路径设置到框中
            ui->filePath->setText(path);
        });
        // 发送文件在子线程中执行, 所以主线程这里只需要发送信号给子线程中的工作对象让其完成发送文件的操作
        connect(ui->btn_sendFile, &QPushButton::clicked, this, [=](){
            emit sendFileSignal(ui->filePath->text());
        });
    
        // 更新进度条的显示数据
        connect(worker, &SendFile::curPercent, ui->ProgressBar_trans, &QProgressBar::setValue);
    
        sub->start();   // 启动子线程
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    
    1. mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private:
        Ui::MainWindow *ui;
    
    signals:
        // 端口和IP
        void startConnect(unsigned short, QString);
    
        // 发送文件的信号
        void sendFileSignal(QString);
    };
    #endif // MAINWINDOW_H
    
    1. sendfile.cpp:准备要在子线程中完成的工作, 连接服务器和发送文件给服务器
    #include "sendfile.h"
    
    #include 
    #include 
    #include 
    
    SendFile::SendFile(QObject *parent) : QObject(parent)
    {
    
    }
    // 当前是在子线程
    void SendFile::connectServer(unsigned short port, QString IP)
    {
        m_tcp = new QTcpSocket;
        m_tcp->connectToHost(QHostAddress(IP),port);
    
        // 若连接成功, 要通知主线程, 主线程弹出提示框 | 这里需要添加自定义的信号进行通知
        connect(m_tcp, &QTcpSocket::connected, this, &SendFile::connectOK);
    
        // 什么时候断开连接
        connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
            // 关闭套接字
            m_tcp->close();
            // 释放资源
            m_tcp->deleteLater();
    
            // 发送信号给主线程, 告诉主线程, 服务器已经和客户端断开了连接
            emit gameOver();
        });
    }
    
    void SendFile::sendFileToServer(QString path)
    {
        QFile file(path);
        QFileInfo info(path);
        // 如果打开文件成功, 再进行下一步
        if (!file.open(QFile::ReadOnly))
        {
            return ;
        }
    
        int fileSize = info.size();
        // 要把当前读取文件的进度, 发送给主窗口, 主线程根据当前的文件进度去更新进度条
        // 服务器那边可以根据文件大小的总和已经达到去判断文件是否读取结束
        // 若没有读完就一直读
        while (!file.atEnd())
        {
            static int num = 0;
            if (num == 0)
            {
                m_tcp->write((char *)&fileSize, 4);
            }
    
            // 读取一行就发送一行
            QByteArray line = file.readLine();
            num += line.size();
    
            // 计算出百分比, 发送给主线程
            int percent = (num * 100 / fileSize);
            emit curPercent(percent);
            m_tcp->write(line);
        }
    }
    
    
    1. sendfile.h
    #ifndef SENDFILE_H
    #define SENDFILE_H
    
    #include 
    #include 
    class SendFile : public QObject
    {
        Q_OBJECT
    public:
        explicit SendFile(QObject *parent = nullptr);
    
        // 连接服务器
        void connectServer(unsigned short port, QString IP);
    
        // 发送文件 | Alt + 回车键, 在源文件中自动生成函数的定义, 进行填写逻辑代码即可
        void sendFileToServer(QString path);
    signals:
        void connectOK();   // 发送通知
        void gameOver();
        void curPercent(int num);
    private:
        QTcpSocket * m_tcp;
    };
    
    #endif // SENDFILE_H
    
    

    六、服务器端的多线程处理

    上面采用了Qt中提供的多线程使用方式的其中一种, 接下来在服务器端使用另外一种较为简单的使用方法, 因为服务器端程序没有太多的功能, 仅仅是在子线程中接收文件;

    1. 操作流程

    1. 需要创建一个线程类的子类, 让其继承QT中的线程类QThread, 比如:
    class MyThread:public QThread
    {
    	......
    }
    
    1. 重写父类的虚函数fun()方法, 在该函数内部编写子线程要处理的具体的业务流程
    class MyThread:public QThread
    {
    	......
    protected:
    	void run()
    	{
    		......
    	}
    }
    
    1. 在主线程中创建子线程对象, new一个
    MyThread * sub = new MyThread;
    
    1. 启动子线程, 调用start()方法
      • 只要调用start()方法, 子线程中run()方法就可以被执行;
    sub->start();
    

    2. 服务器端界面设计

    在这里插入图片描述

    3. 逻辑代码

    1. mainwindow.cpp
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    #include 
    #include 
    #include 
    #include 
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        qDebug() << "服务器主线程:" << QThread::currentThread();
        ui->port->setText("8899");
        m_Server = new QTcpServer(this);
        // 启动监听
        connect(ui->btn_setListen, &QPushButton::clicked, this, [=]()
        {
            unsigned short port = ui->port->text().toUShort();
            m_Server->listen(QHostAddress::Any, port);
        });
    
        // 当有客户端连接到来时, QTcpServer对象会发出newConnection信号
        connect(m_Server, &QTcpServer::newConnection, this, [=]()
        {
            // 得到用于通信的套接字对象
            QTcpSocket * m_tcp = m_Server->nextPendingConnection();
    
            // 创建子线程, 并且将用于通信的套接字对象传参给这个类的构造函数
            recvFile * sub = new recvFile(m_tcp);
    
            sub->start();
    
            // 捕捉over信号
            connect(sub, &recvFile::over, this, [=]()
            {
                sub->exit();
                sub->wait();
                sub->deleteLater();
                QMessageBox::information(this, "文件接收", "文件接收完毕");
            });
        });
    
    
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    
    1. mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    #include 
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private:
        Ui::MainWindow *ui;
        QTcpServer * m_Server;
    };
    #endif // MAINWINDOW_H
    
    
    1. 新建一个recvFile类, 继承自QThread
      • recvfile.cpp
    #include "recvfile.h"
    #include 
    recvFile::recvFile(QTcpSocket * tcp, QObject *parent) : QThread(parent)
    {
        m_tcp = tcp;
    
    }
    
    
    
    // (这里只是一个注册, 不是执行)connect 中的槽函数不知道是什么时候执行的, 而run方法一旦执行, 直接就会走完, 当run方法执行完毕, 子线程的处理流程也就结束了
    // 所以要保证当前的子线程不退出, 要一直检测事件
    void recvFile::run()
    {
        qDebug() << "服务器子线程:" << QThread::currentThread();
        // 接收文件
        QFile * file = new QFile("recv.txt");
        file->open(QFile::WriteOnly);
    
        // 接收数据
        connect(m_tcp, &QTcpSocket::readyRead, this, [=]()
        {
    
            static int count = 0;
            static int total = 0;
            if (count == 0)
            {
                // 如果是第一次就要接收头四个字节的数据(这里面是全部文件的大小数据信息)
                m_tcp->read((char *)&total, 4);
            }
    
            // 读出剩余的数据
            QByteArray all = m_tcp->readAll();
            count += all.size();
            file->write(all);
    
            // 判断数据是否都接收完毕
            if (count == total)
            {
                m_tcp->close();
                m_tcp->deleteLater();
                file->close();
                file->deleteLater();
                emit over();
            }
        });
    
        // 进入事件循环, 不代表子线程退出, 只是到了后台, 当子线程中有对应的事件触发了, 那事件的对应的处理功能还是在子线程中处理
        exec(); // 卡住
    }
    
    
    1. recvfile.h
    #ifndef RECVFILE_H
    #define RECVFILE_H
    
    #include 
    #include 
    #include 
    class recvFile : public QThread
    {
        Q_OBJECT
    public:
        explicit recvFile(QTcpSocket * tcp, QObject *parent = nullptr);
    
    protected:
        // 添加从父类继承来的虚函数 run()
        void run() override;
    signals:
        void over();    // 发射接收结束信号
    private:
        QTcpSocket * m_tcp;
    };
    
    #endif // RECVFILE_H
    
    

    七、 解决通信的套接字无法在子线程中工作的问题

    • 前面, 当用于监听的套接字对象发射出一个newConnection信号, 就说明有客户端发起了连接, 就调用nextPendingConnection()得到一个用于通信的套接字对象;
    • 然后把这个用于通信的套接字对象, 直接传递给了子线程;
      在这里插入图片描述
      但是有可能会遇到通信的套接字对象无法在子线程中工作的问题,下面介绍的就是如何解决? (无论哪种使用子线程的方式, 这里解决的方法一样)
    1. 在QTcpServer中有一个虚函数, incomingConnection():可以得到一个用于通信的文件描述符, 并且这个函数自动被QT框架所调用, 不需要调用nextPendingConnection();
      • 当客户端向服务器发起了连接, incomingConnection()函数就会被调用, 得到一个用于通信的文件描述符;
      • 调用nextPendingConnection(), 其实是对通信的文件描述符进行了封装, 得到一个QTcpSocket对象, 而 incomingConnection()没有对文件描述符进行封装, 所以我们需要自己对通信的文件描述符进行封装;
        在这里插入图片描述
        在这里插入图片描述

    1. 处理流程

    1. 先在当前服务器端的项目中, 新添加一个类MyTcpServer, 继承自QTcpServer
      在这里插入图片描述
    2. 在当前的这个子类中, 重写受保护的虚函数 incomingConnection()
      • mytcpserver.cpp
      #include "mytcpserver.h"
      
      MyTcpServer::MyTcpServer(QObject *parent) : QTcpServer(parent)
      {
      
      }
      
      // 在子线程中通过通信的文件描述符自己创建用于通信的套接字对象
      // 将用于通信的文件描述符, 传递到子线程中 | 用信号发射
      void MyTcpServer::incomingConnection(qintptr handle)
      {
          emit newDescriptor(handle);
      }
      
      
      • mytcpserver.h
      #ifndef MYTCPSERVER_H
      #define MYTCPSERVER_H
      
      #include 
      
      class MyTcpServer : public QTcpServer
      {
          Q_OBJECT
      public:
          explicit MyTcpServer(QObject *parent = nullptr);
      
          void incomingConnection(qintptr handle) override;
      signals:
          void newDescriptor(qintptr SocketDes);
      };
      
      #endif // MYTCPSERVER_H
      
      
    3. 在子线程中将通信的文件描述符封装成QTcpSocket
      • recvfile.cpp
      #include "recvfile.h"
      #include 
      recvFile::recvFile(qintptr sock, QObject *parent) : QThread(parent)
      {
          // 将文件描述符封装成QTcpSocket
          m_tcp = new QTcpSocket(this);
          m_tcp->setSocketDescriptor(sock);
      
      }
      // (这里只是一个注册, 不是执行)connect 中的槽函数不知道是什么时候执行的, 而run方法一旦执行, 直接就会走完, 当run方法执行完毕, 子线程的处理流程也就结束了
      // 所以要保证当前的子线程不退出, 要一直检测事件
      void recvFile::run()
      {
          qDebug() << "服务器子线程:" << QThread::currentThread();
          // 接收文件
          QFile * file = new QFile("recv.txt");
          file->open(QFile::WriteOnly);
      
          // 接收数据
          connect(m_tcp, &QTcpSocket::readyRead, this, [=]()
          {
      
              static int count = 0;
              static int total = 0;
              if (count == 0)
              {
                  // 如果是第一次就要接收头四个字节的数据(这里面是全部文件的大小数据信息)
                  m_tcp->read((char *)&total, 4);
              }
      
              // 读出剩余的数据
              QByteArray all = m_tcp->readAll();
              count += all.size();
              file->write(all);
      
              // 判断数据是否都接收完毕
              if (count == total)
              {
                  m_tcp->close();
                  m_tcp->deleteLater();
                  file->close();
                  file->deleteLater();
                  emit over();
              }
          });
      
          // 进入事件循环, 不代表子线程退出, 只是到了后台, 当子线程中有对应的事件触发了, 那事件的对应的处理功能还是在子线程中处理
          exec(); // 卡住
      }
      
      
      • recvfile.h
      #ifndef RECVFILE_H
      #define RECVFILE_H
      
      #include 
      #include 
      #include 
      class recvFile : public QThread
      {
          Q_OBJECT
      public:
      //    explicit recvFile(QTcpSocket * tcp, QObject *parent = nullptr);
          explicit recvFile(qintptr sock, QObject *parent = nullptr);
      
      protected:
          // 添加从父类继承来的虚函数 run()
          void run() override;
      signals:
          void over();    // 发射接收结束信号
      private:
          QTcpSocket * m_tcp;
      };
      
      #endif // RECVFILE_H
      
      
    4. mainwindow.cpp / .h
    // mainwindow.cpp
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    #include 
    #include 
    #include 
    #include 
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        qDebug() << "服务器主线程:" << QThread::currentThread();
        ui->port->setText("8899");
        m_Server = new MyTcpServer(this);
        // 启动监听
        connect(ui->btn_setListen, &QPushButton::clicked, this, [=]()
        {
            unsigned short port = ui->port->text().toUShort();
            m_Server->listen(QHostAddress::Any, port);
        });
    
        // 当有客户端连接到来时, QTcpServer对象会发出newConnection信号
    //    connect(m_Server, &QTcpServer::newConnection, this, [=]()
    //    {
    //        // 得到用于通信的套接字对象
    //        QTcpSocket * m_tcp = m_Server->nextPendingConnection();
    
    //        // 创建子线程, 并且将用于通信的套接字对象传参给这个类的构造函数
    //        recvFile * sub = new recvFile(m_tcp);
    
    //        sub->start();
    
    //        // 捕捉over信号
    //        connect(sub, &recvFile::over, this, [=]()
    //        {
    //            sub->exit();
    //            sub->wait();
    //            sub->deleteLater();
    //            QMessageBox::information(this, "文件接收", "文件接收完毕");
    //        });
    //    });
    
        // newDescriptor qintptr
        connect(m_Server, &MyTcpServer::newDescriptor, this, [=](qintptr sock)
        {
            // 得到用于通信的套接字对象
    //        QTcpSocket * m_tcp = m_Server->nextPendingConnection();
    
            // 将qintptr类型的文件描述符传递到子线程中, 修改子线程的构造函数;
            recvFile * sub = new recvFile(sock);
    
            sub->start();
    
            // 捕捉over信号
            connect(sub, &recvFile::over, this, [=]()
            {
                sub->exit();
                sub->wait();
                sub->deleteLater();
                QMessageBox::information(this, "文件接收", "文件接收完毕");
            });
        });
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    • mainwindow.h
    // mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    #include 
    #include 
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private:
        Ui::MainWindow *ui;
        MyTcpServer * m_Server;
    };
    #endif // MAINWINDOW_H
    
  • 相关阅读:
    【数据结构基础_字符串】Leetcode 415.字符串相加
    17. 如何通过 SAP ABAP OData $expand 操作在同一个 HTTP 请求中返回多个节点的数据
    广西南宁新能源汽车电机定子三维扫描3D尺寸测量检测-CASAIM中科广电
    JVM八股文
    全网最全JAVA面试八股文,终于整理完了
    STM32F4X之GPIO
    retrofit-spring-boot-starter这款轻量级 HTTP 神器好用到爆
    win10下yolov7 tensorrt模型部署
    Java面试八股文整理
    2022.7.11-7.17 AI行业周刊(第106期):竭尽全力,努力就好
  • 原文地址:https://blog.csdn.net/weixin_43306271/article/details/126939953