目录
一、编写思路
1、服务器
(1)总体思路widget.c(主线程)
(2)详细流程widget.c(主线程)
(1)总体思路chat_thread.c(处理聊天逻辑线程)
(2)详细流程chat_thread.h(处理聊天逻辑线程)
2、客户端
(1)总体思路widget.c(主线程)
(2)详细思路widget.c(主线程)
(1)总体思路chat_thread.c(处理聊天逻辑线程)
(2)详细流程chat_thread.c(处理聊天逻辑线程)
二、实现效果
1、服务器
2、客户端
完整代码请到指定链接下载:Qt网络编程-Tcp多线程并发服务器和客户端通信: 【Qt网络编程】Tcp多线程并发服务器和客户端通信
一、编写思路
1、服务器
(1)总体思路widget.c(主线程)
 
初始化界面
创建窗口、输入框、按钮等基本UI元素。
创建服务器对象
实现
My_tcp_server并监听客户端连接。
处理新客户端连接
当有新客户端连接时,创建新的
Chat_thread线程来处理通信。
绑定信号槽
确保主线程与客户端处理线程间的信号槽连接,使用
Qt::QueuedConnection处理跨线程通信。
处理消息传递
接收和发送消息,并在界面上更新显示。
服务器启动与关闭
通过按钮控制服务器的启动和关闭,管理所有客户端线程的安全退出。
(2)详细流程widget.c(主线程)
 
-  创建 Qt 界面及设置窗口属性: 首先通过 ui->setupUi(this);来初始化用户界面,并设置窗口标题、大小等基本属性。这是 Qt 项目的常见步骤,通过.ui文件生成的类进行界面管理。Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) // 初始化UI对象 {ui->setupUi(this); // 设置UI界面this->setWindowTitle("--服务器--"); // 设置窗口标题this->resize(1024, 960); // 设置窗口大小 ui->le_ip->setText("127.0.0.1");ui->le_port->setText("9999"); }
-  初始化服务器对象 My_tcp_server并处理客户端连接:-  创建 tcp_server对象以处理客户端的连接。
-  使用 connect函数连接tcp_server的new_descriptor信号和匿名槽函数,确保一旦有新客户端连接,便创建一个Chat_thread来处理该客户端。
 this->tcp_server = new My_tcp_server(this);  connect(tcp_server, &My_tcp_server::new_descriptor, this, [=](qintptr socketDescriptor){QMessageBox::information(this, "提示", "新的客户端连接!", QMessageBox::Ok, QMessageBox::Information); ui->btn_send->setEnabled(true); // 启用“发送消息”按钮 // 创建新线程处理客户端Chat_thread *chat_thread = new Chat_thread(socketDescriptor);chat_thread->moveToThread(chat_thread); // 将线程和对象绑定到同一线程,防止冲突 thread_list.append(chat_thread);// 启动线程处理客户端通信chat_thread->start(); });
-  
-  管理客户端线程 Chat_thread:-  每当有新客户端连接时,创建一个 Chat_thread并启动它处理客户端通信。通过moveToThread将Chat_thread的执行线程与该对象保持一致,避免跨线程冲突。
-  使用 connect绑定线程中的信号(如连接断开、接收消息)和主界面槽函数,确保客户端状态能够正确显示。
 Chat_thread *chat_thread = new Chat_thread(socketDescriptor); chat_thread->moveToThread(chat_thread); // 将线程与对象绑定在同一线程  thread_list.append(chat_thread);  // 连接信号和槽 connect(chat_thread, &Chat_thread::break_connect, this, [=](){ui->te_receive->append(currentTime + "\n【状态】客户端断开连接...");ui->btn_send->setEnabled(false); // 禁用“发送消息”按钮 });  connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + "form client\n 【数据】 " + data); // 在文本框中显示消息 });  chat_thread->start(); // 启动线程
-  
-  处理启动和关闭服务器的按钮事件: -  on_btn_connect_clicked()处理连接按钮点击事件,启动或关闭服务器。
-  启动时,检查 IP 地址和端口的有效性,成功后开始监听客户端连接。 
-  关闭服务器时,停止监听,并确保所有已连接客户端线程安全退出。 
 void Widget::on_btn_connect_clicked() {if (!is_server_running){// 启动服务器QString ip_address = ui->le_ip->text().trimmed();QString port_text = ui->le_port->text().trimmed();if (!tcp_server->listen(QHostAddress(ip_address), port_text.toUInt())){QMessageBox::warning(this, "warning", "服务器监听失败");return;}is_server_running = true;ui->btn_connect->setText("关闭服务器");ui->te_receive->append(currentTime + "\n【状态】服务器开始监听...");}else{// 停止服务器并关闭所有客户端线程tcp_server->close();for (Chat_thread *thread : qAsConst(thread_list)){thread->exit();thread->wait();thread->deleteLater();}thread_list.clear();is_server_running = false;ui->btn_connect->setText("创建服务器");ui->te_receive->append(currentTime + "\n【状态】服务器已停止监听...");}}
-  
-  处理发送消息按钮的点击事件: -  当点击“发送消息”按钮时,触发 send_request信号,利用信号槽机制将输入的消息发送给客户端。需要确保主线程和子线程的信号槽通信是异步进行的(通过Qt::QueuedConnection)。
 void Widget::on_btn_send_clicked() {QString data = ui->te_send->toPlainText().toUtf8();emit send_request(data); // 发出 send_request 信号 }
-  
-  服务器监听客户端的状态和信息传递: -  服务器通过 recv_info和send_info信号接收客户端消息并在界面上显示。
-  在客户端连接成功或断开时,更新界面显示状态。 
 connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + "form client\n 【数据】 " + data); // 显示接收的客户端数据 });  connect(chat_thread, &Chat_thread::send_info, this, [=](QString data){currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + "to client\n 【数据】 " + data); // 显示发送给客户端的数据 });
-  
(1)总体思路chat_thread.c(处理聊天逻辑线程)
 
构造函数
初始化
socketDescriptor以供后续线程使用。
线程启动与套接字初始化
在
run()函数中创建QTcpSocket,并关联socketDescriptor。
获取客户端信息
通过
peerAddress()和peerPort()获取客户端 IP 地址和端口号,并进行错误处理。
信号槽机制连接
将套接字状态、接收数据、错误处理等信号连接到相应的槽函数。
处理连接状态变化
通过
handler_client_changed()处理客户端的连接或断开,并发出相应的信号。
处理接收消息
在
receive_message()函数中处理客户端发送的消息,并发出信号recv_info。
发送消息
在
send_message()函数中,检查连接状态并发送消息,发出send_info信号。
错误处理
处理客户端连接中的错误,删除资源并退出线程。
(2)详细流程chat_thread.h(处理聊天逻辑线程)
 
-  构造函数初始化: -  Chat_thread构造函数接受一个socketDescriptor参数,并将其存储为类的成员变量,以供run()函数中使用。注意,QTcpSocket对象将在run()函数中创建,以确保在新线程中创建并使用。
 Chat_thread::Chat_thread(qintptr socketDescriptor, QObject *parent): QThread{parent}, socketDescriptor(socketDescriptor) {// socketDescriptor 存储为成员变量 }
-  
-  线程启动和套接字初始化: -  在 run()函数中创建QTcpSocket对象,并通过setSocketDescriptor()将套接字描述符与QTcpSocket关联。这允许线程使用此套接字与客户端通信。
-  如果套接字初始化失败,进行错误处理并返回。 
 void Chat_thread::run() {// 创建 QTcpSocket 对象,用于处理与客户端的通信this->socket = new QTcpSocket(); // 将套接字描述符与 QTcpSocket 关联if (!socket->setSocketDescriptor(socketDescriptor)){qDebug() << "Error: Failed to get new socketDescriptor.";return;} // 错误处理:检查是否成功获取客户端连接if (socket == nullptr){qDebug() << "Error: Failed to get new client connection.";return; // 如果获取失败,直接返回} }
-  
-  获取客户端信息: -  在成功创建套接字后,获取客户端的 IP 地址和端口号。 
-  如果获取失败,进行错误处理并断开连接。 
 // 获取客户端的IP地址和端口号 QString ip_addr = socket->peerAddress().toString(); quint16 port = socket->peerPort();  // 错误处理:检查是否成功获取IP地址和端口号 if (ip_addr.isEmpty() || port == 0) {qDebug() << "Error: Failed to get client's IP address or port.";socket->disconnectFromHost(); // 断开连接socket->deleteLater(); // 删除客户端套接字对象return; // 如果获取失败,直接返回 }
-  
-  信号槽机制的连接: -  连接套接字的状态改变信号 stateChanged到槽函数handler_client_changed,以便监控客户端连接状态的变化。
-  连接 QTcpSocket的readyRead信号到receive_message槽函数,用于处理接收数据。
-  处理套接字错误时,连接 errorOccurred信号到handle_socket_error槽函数。
 // 处理连接状态变化的槽函数 connect(socket, &QTcpSocket::stateChanged, this, &Chat_thread::handler_client_changed);  // 错误处理:处理客户端的异常断开情况 connect(socket, &QTcpSocket::errorOccurred, this, &Chat_thread::handle_socket_error);  // 处理接收数据的槽函数 connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message);
-  
-  处理客户端连接状态变化: -  在 handler_client_changed()槽函数中,根据客户端的连接状态(如断开、已连接)做相应处理并发出信号,通知其他部分更新状态。
 void Chat_thread::handler_client_changed(QAbstractSocket::SocketState socket_state) {socket = (QTcpSocket*)sender(); // 获取发信的客户端套接字if(!socket) return; switch (socket_state){case QAbstractSocket::UnconnectedState: // 客户端断开连接emit break_connect();break; case QAbstractSocket::ConnectedState: // 客户端已连接emit complete_connect();break; default:break;} }
-  
-  接收消息的处理: -  在 receive_message()槽函数中,通过socket->readAll()读取客户端发送的所有数据,并发出信号recv_info通知上层处理。
 void Chat_thread::receive_message() {if (socket){QString data = socket->readAll(); // 读取客户端发送的所有数据emit recv_info(data); // 发出信号,通知收到消息} }
-  
-  发送消息: -  在 send_message()函数中,检查客户端是否处于连接状态,如果是则发送消息,否则输出警告信息。
-  发送完成后,发出 send_info信号。
 void Chat_thread::send_message(QString data) {if (socket->state() == QAbstractSocket::ConnectedState){socket->write(data.toUtf8()); // 发送数据}else{qDebug() << "warning: 客户端未连接,无法发送消息"; // 输出警告} emit send_info(data); // 发出信号,通知发送消息 }
-  
-  错误处理: -  在 handle_socket_error()函数中处理QTcpSocket的错误。如果出现错误,打印错误信息,并退出线程。
-  删除套接字对象并退出线程事件循环。 
 void Chat_thread::handle_socket_error(QAbstractSocket::SocketError socketError) {qDebug() << "Client connection error, error code: " << socketError; this->exit(); // 退出线程this->wait(); // 等待线程完全退出socket->deleteLater(); // 删除客户端套接字对象 // 停止线程事件循环quit(); }
-  
2、客户端
(1)总体思路widget.c(主线程)
 
初始化界面
设置窗口属性并初始化用户输入的默认值。
创建线程和通信任务对象
实现异步通信,使用
QThread和自定义Chat_thread处理服务器交互。
信号槽机制的建立
连接 UI 和工作线程之间的信号槽,确保各操作异步处理。
线程管理
在析构函数中确保线程安全退出,释放资源。
处理连接与断开
通过按钮触发连接和断开操作,并更新 UI 显示。
消息传递与显示
处理消息的发送与接收,并在 UI 界面上更新显示结果。
(2)详细思路widget.c(主线程)
 
-  初始化界面: -  使用 ui->setupUi(this)初始化用户界面,并设置窗口标题和窗口大小。
-  初始化 IP 地址和端口号的默认值。 
 Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), is_connected(false) // 初始化连接状态为未连接 {ui->setupUi(this); // 设置UI界面this->setWindowTitle("-客户端-"); // 设置窗口标题this->resize(1024, 960); // 设置窗口大小 ui->le_ip->setText("127.0.0.1"); // 设置默认IP地址ui->le_port->setText("8888"); // 设置默认端口号 }
-  
-  创建线程和通信任务对象: -  创建 QThread对象以进行异步通信任务。
-  创建 Chat_thread对象负责与服务器进行通信操作。
-  使用 moveToThread将通信任务对象移到新的线程中执行,并启动该线程。
 // 创建线程对象 thread = new QThread;  // 创建任务对象,负责与服务器的通信 Chat_thread *worker = new Chat_thread;  worker->moveToThread(thread); // 将任务对象移至线程 thread->start(); // 启动工作线程
-  
-  信号槽机制的建立: -  使用信号槽连接 UI 和工作线程之间的交互。例如,连接服务器、发送消息、断开连接等操作通过信号槽机制进行。 
-  信号从 UI 线程发出,工作线程的槽函数接收信号并执行相关操作。 
 // 信号槽连接:从UI线程发出连接信号,worker线程接收并执行连接操作 connect(this, &Widget::connect_server, worker, &Chat_thread::start_connected); connect(this, &Widget::send_info, worker, &Chat_thread::start_send); connect(this, &Widget::quit_connect, worker, &Chat_thread::break_connected);  // 连接断开信号槽,worker线程通知UI线程更新UI connect(worker, &Chat_thread::connect_cancel, this, &Widget::submit_connect_cancel); connect(worker, &Chat_thread::connected, this, &Widget::submit_connect_info); connect(worker, &Chat_thread::transfer_recv_info, this, &Widget::submit_recv_info);
-  
-  管理线程的生命周期: -  在析构函数中,确保工作线程在窗口关闭时被正确停止,并释放相关资源。 
-  如果线程正在运行,需要先请求线程退出,然后等待其完全退出后再删除。 
 Widget::~Widget() {if (thread->isRunning()){thread->quit(); // 请求线程退出thread->wait(); // 等待线程结束}delete worker; // 删除任务对象delete thread; // 删除线程对象delete ui; // 删除UI对象 }
-  
-  处理连接成功或断开连接的槽函数: -  当客户端成功连接到服务器时,工作线程发出 connected信号,UI 界面通过槽函数submit_connect_info()来更新显示状态,并启用“发送消息”按钮。
-  断开连接时,UI 界面通过槽函数 submit_connect_cancel()来禁用“发送消息”按钮,并更新状态显示。
 // 连接成功时的槽函数,更新UI显示信息 void Widget::submit_connect_info() {currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + "\n【状态】已成功连接到服务器");ui->btn_connect->setText("断开服务器");ui->btn_send->setEnabled(true); // 启用发送按钮is_connected = true; }  // 断开连接时的槽函数,更新UI显示信息 void Widget::submit_connect_cancel() {is_connected = false; }
-  
-  处理消息的发送与接收: -  当用户点击“发送消息”按钮时,获取文本框中的消息,发出 send_info信号,将消息发送到服务器。
-  当从服务器接收到消息时,工作线程发出 transfer_recv_info信号,UI 界面更新显示接收到的消息。
 // 当用户点击发送按钮时,读取输入框中的内容并发送给服务器 void Widget::on_btn_send_clicked() {currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");QString message = ui->te_send->toPlainText().toUtf8(); // 获取用户输入的消息ui->te_receive->append(currentTime + " to server\n 【数据】" + message + "\n");emit send_info(message); // 发出信号,通知工作线程发送消息 }  // 当接收到服务器发送的消息时,更新UI显示接收到的消息 void Widget::submit_recv_info(QString message) {currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + " form server\n 【数据】" + message + "\n"); // 显示服务器的消息 }
-  
-  处理连接与断开的按钮事件: -  当点击“连接”按钮时,获取 IP 地址和端口号,检查输入的有效性后发出 connect_server信号,通知工作线程与服务器建立连接。
-  当点击“断开服务器”按钮时,发出 quit_connect信号,通知工作线程断开连接。
 // 当用户点击"连接"按钮时触发该槽函数 void Widget::on_btn_connect_clicked() {if (!is_connected){QString ip_address = ui->le_ip->text().trimmed();QString port_text = ui->le_port->text().trimmed(); QHostAddress address;if (!address.setAddress(ip_address)) // 检查IP地址的有效性{QMessageBox::warning(this, "warning", "无效的IP地址,请重新输入!");return;} bool ok;unsigned int port = port_text.toUInt(&ok);if (!ok || port == 0 || port > 65535) // 检查端口号的有效性{QMessageBox::warning(this, "warning", "无效的端口号,请输入1到65535之间的数值!");return;} emit connect_server(ip_address, port); // 发出连接服务器的信号}else{currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");ui->te_receive->append(currentTime + "\n【状态】已断开与服务器的连接");ui->btn_send->setEnabled(false); // 禁用发送按钮is_connected = false;emit quit_connect(); // 发出断开连接的信号} }
-  
(1)总体思路chat_thread.c(处理聊天逻辑线程)
 
构造函数
初始化
Chat_thread对象。
接收消息
通过
readyRead信号槽接收服务器发送的数据,并将其转发给主线程。
处理连接状态变化
监控与服务器的连接状态,并打印调试信息。
断开连接
关闭套接字连接并释放资源,发出连接断开信号。
启动连接
通过指定的 IP 和端口号连接服务器,并处理连接成功、失败、断开、接收数据等事件。
发送消息
检查连接状态并发送消息。如果未连接,则发出未连接信号。
(2)详细流程chat_thread.c(处理聊天逻辑线程)
 
-  构造函数: -  构造函数 Chat_thread::Chat_thread(QObject *parent)初始化Chat_thread对象。在这个阶段不需要任何复杂的逻辑,主要是确保对象正常创建。
 Chat_thread::Chat_thread(QObject *parent): QObject{parent} {}
-  
-  接收消息处理: -  receive_message()是一个槽函数,用于接收从服务器发送的数据。当QTcpSocket对象有数据可读取时,信号readyRead会被触发,调用此槽函数。读取数据后,通过transfer_recv_info信号将接收到的消息发送出去。
 void Chat_thread::receive_message() {QString message = socket->readAll(); // 从服务器读取数据emit transfer_recv_info(message); // 发出信号,通知接收到的数据 }
-  
-  处理连接状态变化: -  state_changed()函数是一个槽函数,用于处理客户端与服务器的连接状态变化。根据不同的QAbstractSocket::SocketState枚举值,打印调试信息并处理相应状态的变化。
 void Chat_thread::state_changed(QAbstractSocket::SocketState socketstate) {QString stateStr; // 用于保存状态的字符串switch (socketstate){case QAbstractSocket::UnconnectedState:qDebug()<< "\n【状态】与服务器断开连接...";stateStr = "UnconnectedState";break; case QAbstractSocket::ConnectedState:stateStr = "ConnectedState";qDebug()<< "【状态】与服务器建立连接...";break; case QAbstractSocket::HostLookupState:stateStr = "HostLookupState";qDebug()<< "【状态】正在查找主机...";break; case QAbstractSocket::ConnectingState:stateStr = "ConnectingState";qDebug()<< "【状态】正在连接服务器...";break; case QAbstractSocket::ClosingState:stateStr = "ClosingState";qDebug()<< "【状态】正在关闭连接...";break; default:stateStr = "UnknownState";qDebug()<< "未知的错误, 当前状态: " + stateStr;break;} }
-  
-  断开连接处理: -  break_connected()用于处理断开与服务器的连接。当套接字连接断开时,关闭并释放资源,并发出connect_cancel信号通知主线程。
 void Chat_thread::break_connected() {socket->close(); // 关闭套接字socket->deleteLater(); // 延迟删除套接字,释放资源emit connect_cancel(); // 发出连接断开信号 }
-  
-  开始连接服务器: -  start_connected()用于发起连接服务器的请求。创建QTcpSocket对象并尝试连接到指定的 IP 和端口。连接成功、失败、断开、接收数据等事件都会通过信号槽机制进行处理。
 void Chat_thread::start_connected(QString IP, unsigned short PORT) {socket = new QTcpSocket; // 创建套接字对象 socket->connectToHost(QHostAddress(IP), PORT); // 连接到服务器 // 连接成功时,发送 connected 信号通知主线程上传消息connect(socket, &QTcpSocket::connected, this, &Chat_thread::connected); // 连接失败时处理connect(socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError socketError){qDebug() << "连接失败";QMessageBox::critical(nullptr, "连接失败", "连接失败,错误代码:" + QString::number(socketError));}); // 连接断开时处理connect(socket, &QTcpSocket::disconnected, this, &Chat_thread::break_connected); // 监听数据接收connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message); }
-  
-  发送消息: -  start_send()用于发送消息到服务器。首先检查套接字是否处于连接状态,如果已连接,则发送消息。如果未连接,则发出not_connected信号。
 void Chat_thread::start_send(QString message) {if (socket && socket->state() == QAbstractSocket::ConnectedState){socket->write(message.toUtf8()); // 发送消息}else{emit not_connected(); // 如果未连接,发出未连接信号} }
-  
二、实现效果
1、服务器

2、客户端
