在上一篇文章的基础上,本文只把 客户端的单线程 改为多线程,暂不增加 文件传输。避免一次改太多。暂不改动 服务器端 单线程代码。
如果 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);
}