Qt 简易聊天室(第二篇),单线程 改为 多线程,方便后续增加文件传输功能

在上一篇文章的基础上,本文只把 客户端的单线程 改为多线程,暂不增加 文件传输。避免一次改太多。暂不改动 服务器端 单线程代码。

如果 UI 绘制、socket 网络 I/O、磁盘文件读写都放在主线程,传输大文件时 磁盘读写会阻塞事件循环,导致界面卡顿。所以把 socket 收发和文件传输移到子线程,主线程只负责 UI 和信号协调,为后续文件传输打好基础。

客户端代码:

main.cpp 文件

复制代码
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

mainwindow.cpp 文件

复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "networkworker.h"
#include <QHostAddress>
#include <QThread>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    setWindowTitle("客户端");
    ui->port->setText("8899");
    ui->ip->setText("127.0.0.1");
    ui->disconnectServer->setEnabled(false);
    ui->record->setReadOnly(true);

    // 状态栏
    m_statusPixmap = new QLabel;
    m_statusPixmap->setPixmap(QPixmap(":/disconnect").scaled(20, 20));
    m_statusText = new QLabel("连接状态: ");
    ui->statusbar->addWidget(m_statusText);
    ui->statusbar->addWidget(m_statusPixmap);

    // 子线程 + NetworkWorker
    m_netWorkThread = new QThread(this);
    m_netWorker = new NetworkWorker;
    m_netWorker->moveToThread(m_netWorkThread);

    // 线程结束时自动销毁 worker
    connect(m_netWorkThread, &QThread::finished, m_netWorker, &QObject::deleteLater);

    connect(this, &MainWindow::connectToServer, m_netWorker, &NetworkWorker::connectToServer);
    connect(this, &MainWindow::disconnectFromServer, m_netWorker, &NetworkWorker::disconnectFromServer);
    connect(m_netWorker, &NetworkWorker::connected, this, [=]() {
        QString name = ui->clientName->text().trimmed();
        emit sendMessage("LOGIN:" + name.toUtf8() + "\n");
        // 状态栏更新
        m_statusPixmap->setPixmap(QPixmap(":/connect").scaled(20, 20));
        ui->record->append("已连接服务器");
    });

    connect(m_netWorker, &NetworkWorker::disconnected, this, [=]() {
        m_statusPixmap->setPixmap(QPixmap(":/disconnect").scaled(20, 20));
        m_loggedIn = false;
        ui->disconnectServer->setEnabled(false);
        ui->connectServer->setEnabled(true);
        ui->targetCombox->clear();  // 清空在线用户列表
        ui->record->append("服务器断开连接...");
    });

    connect(m_netWorker, &NetworkWorker::errorOccurred, this, [=](const QString &err) {
        ui->record->append("连接失败: " + err);
    });

    connect(this, &MainWindow::sendMessage, m_netWorker, &NetworkWorker::sendMessage);
    // 收到按行解析的数据
    connect(m_netWorker, &NetworkWorker::dataReceived, this, &MainWindow::processLine);

    networkThread->start();
}

MainWindow::~MainWindow()
{
    // 安全退出线程
    m_netWorkThread->quit();
    m_netWorkThread->wait();
    delete ui;
}

void MainWindow::processLine(const QByteArray &line)
{
    static const QByteArray REQ_LIST = "clientRequestList:";
    static const QByteArray LOGIN_OK = "LOGIN_OK:";
    static const QByteArray LOGIN_FAIL = "LOGIN_FAIL:";

    // 保存在线用户列表
    if(line.startsWith(REQ_LIST)) {
        QString list = QString(line.mid(REQ_LIST.length()).trimmed());
        if (list.isEmpty())
            return;
        QStringList users = list.split(",");    // 用逗号拆开字符串
        ui->targetCombox->clear();
        ui->targetCombox->addItem("所有人");
        ui->targetCombox->addItems(users);  // 批量添加多项
        return;
    }

    if (line.startsWith(LOGIN_OK)) {
        m_loggedIn = true;
        ui->record->append("登录成功");
        ui->connectServer->setEnabled(false);
        ui->disconnectServer->setEnabled(true);
        return;
    }

    if (line.startsWith(LOGIN_FAIL)) {
        ui->record->append("登录失败: " + line.mid(LOGIN_FAIL.length()));
        emit disconnectFromServer();
        return;
    }
    // 显示普通消息
    ui->record->append(line);
}

void MainWindow::on_connectServer_clicked()
{
    // 校验 IP 和 port
    QString ip = ui->ip->text();
    QHostAddress addr(ip);
    if(addr.isNull() || addr.protocol() == QAbstractSocket::UnknownNetworkLayerProtocol) {
        ui->record->append("IP 地址无效");
        return;
    }

    unsigned short port = ui->port->text().toUShort();
    if(port == 0) {
        ui->record->append("端口无效, 请输入: 1 - 65535");
        return;
    }

    // 必须设置客户端名字, 区分不同的客户端
    QString name = ui->clientName->text().trimmed();
    if (name.isEmpty()) {
        ui->record->append("请先输入 客户端名字");
        return;
    }

    emit connectToServer(ip, port);
    ui->record->append("正在连接 " + ip + " : " + QString::number(port));
}

void MainWindow::on_disconnectServer_clicked()
{
    emit disconnectFromServer();
}

void MainWindow::on_sendMsg_clicked()
{
    if (!m_loggedIn) {
        ui->record->append("发送失败, 请先登录");
        return;
    }

    QString msg = ui->msg->toPlainText().trimmed();
    if (msg.isEmpty()) {
        ui->record->append("空消息,发送失败");
        return;
    }

    QString target = ui->targetCombox->currentText();
    QByteArray data;
    if(target == "所有人") {
        data = "clientMsgAll:" + msg.toUtf8();
    }
    else {
        data = "clientMsgTo:" + target.toUtf8() + ":" + msg.toUtf8();
        ui->record->append("我 -> " + target + ": " + msg );
    }
    emit sendMessage(data + "\n");
}

mainwindow.h 文件

复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QByteArray>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_connectServer_clicked();

    void on_disconnectServer_clicked();

    void on_sendMsg_clicked();

    void processLine(const QByteArray &line);


private:
    Ui::MainWindow *ui;
    QLabel* m_statusPixmap = nullptr;
    QLabel* m_statusText = nullptr;
    bool m_loggedIn = false;
    // 子线程
    QThread *m_netWorkThread = nullptr;
    // 任务对象
    NetworkWorker *m_netWorker = nullptr;

signals:
    void sendMessage(const QByteArray &data);
    void connectToServer(const QString &ip, quint16 port);
    void disconnectFromServer();
};
#endif // MAINWINDOW_H

networkworker.cpp 文件

复制代码
#include "networkworker.h"

#include <QHostAddress>

NetworkWorker::NetworkWorker(QObject *parent)
    : QObject{parent}
{
    m_socket = new QTcpSocket(this);

    connect(m_socket, &QTcpSocket::connected, this, &NetworkWorker::connected);
    connect(m_socket, &QTcpSocket::disconnected, this, &NetworkWorker::disconnected);

    connect(m_socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError) {
        emit errorOccurred(m_socket->errorString());
    });

    // 接收数据
    connect(m_socket, &QTcpSocket::readyRead, this, [=]() {
        m_buffer.append(m_socket->readAll());

        while(true) {
            int idx = m_buffer.indexOf('\n');
            if(idx == -1)
                break;

            QByteArray line = m_buffer.left(idx).trimmed();
            m_buffer.remove(0,idx + 1);

            if(line.isEmpty())
                continue;
            emit dataReceived(line);    // 按行解析后逐行发出
        }
    });
}

NetworkWorker::~NetworkWorker()
{
    m_socket->close();
}

void NetworkWorker::connectToServer(const QString &ip,quint16 port)
{
    m_socket->connectToHost(QHostAddress(ip), port);
}

void NetworkWorker::disconnectFromServer()
{
    m_socket->close();
}

void NetworkWorker::sendMessage(const QByteArray &data)
{
    m_socket->write(data);
}

networkworker.h 文件

复制代码
#ifndef NETWORKWORKER_H
#define NETWORKWORKER_H

#include <QObject>
#include <QTcpSocket>

class NetworkWorker : public QObject
{
    Q_OBJECT
public:
    explicit NetworkWorker(QObject *parent = nullptr);
     ~NetworkWorker();

public slots:
    void connectToServer(const QString &ip,quint16 port);
    void disconnectFromServer();
    void sendMessage(const QByteArray &data);

signals:
    void connected();
    void disconnected();
    void errorOccurred(const QString &error);
    void dataReceived(const QByteArray &line);

private:
    QTcpSocket *m_socket;
    QByteArray m_buffer;
};

#endif // NETWORKWORKER_H

服务器端 mainwindow.cpp 文件,完整代码

复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    setWindowTitle("服务器");
    ui->port->setText("8899");
    ui->stopListen->setEnabled(false);
    ui->record->setReadOnly(true);

    // 创建服务器对象,只连接一次
    m_tcpServer = new QTcpServer(this);
    connect(m_tcpServer, &QTcpServer::newConnection, this, &MainWindow::onNewConnection);

    // 状态栏
    m_statusPixmap = new QLabel;
    m_statusText = new QLabel("连接状态: ");
    m_statusPixmap->setPixmap(QPixmap(":/tcp_disconnect").scaled(20, 20));
    ui->statusbar->addWidget(m_statusText);
    ui->statusbar->addWidget(m_statusPixmap);

}

MainWindow::~MainWindow()
{
    delete ui;
}


void MainWindow::on_setListen_clicked()
{
    unsigned short port = ui->port->text().toUShort();
    if(port == 0) {
        ui->record->append("端口号无效,请输入 1-65535");
        return;
    }

//    if(m_tcpServer->listen(QHostAddress::Any, port))
    if(m_tcpServer->listen(QHostAddress::AnyIPv4, port)) {
        quint16 realPort = m_tcpServer->serverPort();   // 获取实际分配的端口
        ui->record->append("开始监听,端口: " + QString::number(realPort));
        ui->setListen->setEnabled(false);
        ui->stopListen->setEnabled(true);
    }
    else {
        // 监听失败(端口被占用、权限不足等)
        ui->record->append("监听失败: " + m_tcpServer->errorString());
    }
}

void MainWindow::on_stopListen_clicked()
{
    // 断开 所有客户端
    for(QTcpSocket* client : m_clients) {
        client->close();    // disconnected 信号会自动清理列表
    }

    if(m_tcpServer->isListening()) {
        m_tcpServer->close();
    }

    ui->setListen->setEnabled(true);
    ui->stopListen->setEnabled(false);
    ui->record->append("已停止监听");
}

// 客户端连接服务器
void MainWindow::onNewConnection()
{
    // 取出 QTcpServer内部 已经创建好的资源, 获取下一个等待连接的客户端套接字
    QTcpSocket *client = m_tcpServer->nextPendingConnection();
    // 加入列表
    m_clients.append(client);

    m_statusPixmap->setPixmap(QPixmap(":/tcp_connect").scaled(20, 20));

    // 获取客户端 IP 和端口
    QString clientIp = client->peerAddress().toString();
    quint16 clientPort = client->peerPort();
    QString clientInfo = clientIp + ":" + QString::number(clientPort);

    // 下拉框显示 clientInfo , 并且 socket指针 设置为下拉框的隐藏数据
    ui->clientCombox->addItem(clientInfo,
                QVariant::fromValue<quintptr>(reinterpret_cast<quintptr>(client)));

    bool *named = new bool(false);
    QByteArray *buffer = new QByteArray;
    // 接收到数据
    connect(client, &QTcpSocket::readyRead, this, [=]() {
        static const QByteArray CLIENTMSGALL = "clientMsgAll:";
        static const QByteArray CLIENTMSGTO = "clientMsgTo:";
        static const QByteArray LOGIN = "LOGIN:";
        static const QByteArray FILEREQUEST = "FILEREQUEST:";

        // 定义一个局部辅助函数,专门用来通过 socket 找列表中的名字
        auto getNameBySocket = [&](QTcpSocket* targetSock) -> QString {
            for(int i = 0; i < ui->clientCombox->count(); i++) {
                QTcpSocket* sock = reinterpret_cast<QTcpSocket*>(
                    ui->clientCombox->itemData(i).value<quintptr>());
                if(sock == targetSock)
                    return ui->clientCombox->itemText(i);
            }
            return "未知用户";
        };

        buffer->append(client->readAll());

        while(true) {
            int idx = buffer->indexOf('\n');   // 标记:行尾在哪里
            if(idx == -1)
                break;

            QByteArray line = buffer->left(idx).trimmed();
            // 从缓冲区头部删除已经处理完的一行数据(+1 包括换行符),为下一次处理做准备
            buffer->remove(0, idx + 1);

            if(line.startsWith(LOGIN)) {
                QString name =  QString(line.mid(LOGIN.length())).trimmed(); // 去掉首尾空白(\n、空格等)
                // 验证,检查重名
                bool exists = false;
                for(int i = 0; i < ui->clientCombox->count(); i++) {
                    if(ui->clientCombox->itemText(i) == name) {
                        exists = true;
                        break;
                    }
                }
                if(exists) {
                    client->write(("LOGIN_FAIL:名字 " + name + " 已被占用" + "\n").toUtf8());
                    client->close();
                    continue;
                }
                else {
                    *named = true;
                    int currentRow = -1;
                    for(int i = 0; i < ui->clientCombox->count(); i++) {
                        // 取出第 i 行隐藏的 socket 指针
                        QTcpSocket* sock = reinterpret_cast<QTcpSocket*>(
                            ui->clientCombox->itemData(i).value<quintptr>());
                        if(sock == client) {
                            currentRow = i;
                            break;
                        }
                    }

                    if(currentRow != -1){
                        // 修改已有的显示文字, 只显示客户端名字,区分不同的在线客户端
                        ui->clientCombox->setItemText(currentRow, name);
                    }
                    client->write("LOGIN_OK:\n");
                    ui->record->append("上线: " + name);

                    // 新客户端上线后,服务器推送在线列表 -> 给所有已上线的客户端
                    for (QTcpSocket *c : m_clients) {
                        QStringList names;
                        for (int i = 0; i < ui->clientCombox->count(); i++) {
                              names.append(ui->clientCombox->itemText(i));
                        }
                        c->write(("clientRequestList:" + names.join(",") + "\n").toUtf8());
                    }
                    continue;
                }
            }

            // 客户端 向其他客户端 群发消息
            if(line.startsWith(CLIENTMSGALL)) {
                QByteArray msg = line.mid(CLIENTMSGALL.length()).trimmed();
                QString sender = getNameBySocket(client);
                for(QTcpSocket *c : m_clients) {
                        c->write((sender + "(群发): " + msg + "\n").toUtf8());
                }
                ui->record->append(sender + "(群发): " + msg);
                continue ;
            }

            // 客户端与客户端 私发消息
            if(line.startsWith(CLIENTMSGTO)) {
                // 示例 clientMsgTo:Bob:你好
                QByteArray payload = line.mid(CLIENTMSGTO.length()).trimmed();  // "Bob:你好"
                int sep = payload.indexOf(':');
                if(sep == -1)
                    continue;
                QString target = payload.left(sep); // "Bob"
                QByteArray msg = payload.mid(sep+1);// "你好"
                QString sender = getNameBySocket(client);

                // 从下拉框找到私发目标
                for (int i = 0; i < ui->clientCombox->count(); i++) {
                      QString name = ui->clientCombox->itemText(i);  // 提取 "Bob"

                    if (name == target) {
                        QTcpSocket *sock = reinterpret_cast<QTcpSocket*>(
                            ui->clientCombox->itemData(i).value<quintptr>());
                        sock->write((sender + ": " + msg + "\n").toUtf8());
                        ui->record->append(sender + " -> " + target + ": " + msg);
                        break;
                    }
                }
                continue ;
            }

            // 普通消息时,从下拉框获取当前名字
            QString senderName = getNameBySocket(client);
            ui->record->append(senderName + ": " + line);
        }
    });

    // 断开连接
    connect(client, &QTcpSocket::disconnected, this, [=]() {
        QString clientName;
        // 客户端断开时, 从下拉框移除
        for(int i = 0; i < ui->clientCombox->count(); i++) {
            QTcpSocket* sock = reinterpret_cast<QTcpSocket*>(
                ui->clientCombox->itemData(i).value<quintptr>());
            if(sock == client) {
                clientName = ui->clientCombox->itemText(i);  // 找到名字
                ui->clientCombox->removeItem(i);
                break;
            }
        }

        // 根据状态显示
        if(*named && !clientName.isEmpty()) {
            ui->record->append("下线: " + clientName);
        }
        else {
            ui->record->append("客户端: " + clientInfo + " 名字未通过验证,被踢下线");
        }

        m_clients.removeOne(client);
        client->deleteLater(); // 将对象标记为待删除, 在事件循环空闲时 销毁对象
        delete named;  // 清理标志位
        delete buffer; // 清理缓冲区


        if(m_clients.isEmpty()) {
            m_statusPixmap->setPixmap(QPixmap(":/tcp_disconnect").scaled(20, 20));
        }

        // 客户端断开后, 主动向所有在线用户,推送更新
        for(QTcpSocket *c : m_clients) {
            QStringList names;
            for(int i = 0; i < ui->clientCombox->count(); i++) {
                names.append(ui->clientCombox->itemText(i));
            }
            c->write(("clientRequestList:" + names.join(",") + "\n").toUtf8());
        }
    });
}

// 服务器私发
void MainWindow::on_sendMsg_clicked()
{
    if (m_clients.isEmpty() || ui->clientCombox->currentIndex() < 0) {
        ui->record->append("没有客户端连接,无法发送");
        return;
    }

    // 从下拉框获取选中的 socket
    QTcpSocket *selected = reinterpret_cast<QTcpSocket*>(
        ui->clientCombox->currentData().value<quintptr>());

    if (!selected || selected->state() != QAbstractSocket::ConnectedState) {
        ui->record->append("选中的客户端已断开");
        return;
    }

    QString msg = ui->msg->toPlainText().toUtf8();
    if (msg.isEmpty()) {
        ui->record->append("空消息,无法发送");
        return;
    }
    selected->write("服务器: " + msg.toUtf8() + "\n");
    ui->record->append("服务器 → " + ui->clientCombox->currentText() + ": " + msg);
}

// 服务器群发
void MainWindow::on_sendMsgAll_clicked()
{
    if (m_clients.isEmpty()) {
        ui->record->append("没有客户端连接,无法发送");
        return;
    }

    QString msg = ui->msg->toPlainText().toUtf8();
    if (msg.isEmpty()) {
        ui->record->append("空消息,无法发送");
        return;
    }
    for (QTcpSocket *client : m_clients) {
        client->write("服务器群发: " + msg.toUtf8() + "\n");
    }
    ui->record->append("服务器群发: " + msg);
}
相关推荐
Tyler_TXZ2 小时前
C++C语言之——二叉树
c语言·开发语言·数据结构·c++·二叉树
有点。2 小时前
C++二叉树二(练习题)
数据结构·c++·算法·图论
不会代码的小猴2 小时前
C++新增关键字
开发语言·c++·笔记
liulilittle3 小时前
并发与内存安全术语:定义、分类与关联
c++·安全·并发·术语
_wyt0013 小时前
从图到树:9道洛谷基础题带你入门树形结构
c++·
Titan20244 小时前
Linux网络基础知识
linux·服务器·网络·c++·学习
丰锋ff5 小时前
1.7在Qt窗口中添加右键菜单
开发语言·qt
Titan20246 小时前
Linux网络学习:套接字、UDP的封装与应用
linux·服务器·开发语言·网络·c++
Felven6 小时前
C2. Potions (Hard Version)
c++