QT_实战TCP 聊天程序说明文档以及实现
TCP 聊天程序说明文档本文档详细讲解QTcpServerpro服务端与QTcpClientPros客户端两个 Qt 项目的实现原理、运行步骤、核心 API 与完整代码流程适合逐行对照源码仔细琢磨。案例代码git仓库地址gitgithub.com:wuyongGitHub/QtTcpServerDemo-QtTcpChatDemo.git目录文章目录TCP 聊天程序说明文档目录一、项目概述二、开发环境三、整体架构与通信原理3.1 三个核心对象3.2 通信流程时序3.3 为什么消息末尾要加换行符 \n四、服务端详解4.1 文件结构4.2 mainwindow.h 成员变量4.3 构造函数做了什么4.4 核心槽函数逐个讲解1GetLocalIpAddress() —— 获取本机 IPv42on_start_btn_clicked() —— 启动服务3newconnection() —— 接受新连接核心4currentSocket() —— 定位信号来源5socketreaddata() —— 接收并转发群聊核心6clientdisconnect() —— 客户端下线7on_send_btn_clicked() —— 服务器广播8closeEvent() —— 优雅退出五、客户端详解5.1 文件结构5.2 构造函数一次连接、避免重复5.3 核心槽函数1on_ConnectBtn_clicked() —— 发起连接2onConnected() / onDisconnected()3on_sendMsgbtn_clicked() —— 发送4onReadyRead() —— 接收5onSocketError() —— 出错提示6closeEvent() —— 优雅退出六、群聊消息如何互通七、编译与运行步骤7.1 编译7.2 运行7.3 本机自测小技巧八、核心 Qt API 速查表QTcpServer服务器QTcpSocket通信套接字客户端与服务端通用QTcpSocket 常用信号QAbstractSocket::SocketState 枚举state() 返回值其他辅助类信号槽两种写法对比九、常见问题排查一、项目概述项目角色职责QTcpServerpro服务端监听端口、接受多个客户端连接、接收并转发消息群聊中心QTcpClientPros客户端连接服务器、发送消息、接收服务器转发来的消息最终效果服务端启动后多个客户端连接上来任意一个客户端发的消息其余所有客户端以及服务器都能看到实现一个简易的TCP 群聊系统。二、开发环境开发框架Qt 5.7 及以上本机为 Qt 5.12.8 MinGW必需模块core、gui、network.pro文件中已声明QT network语言CQt 信号槽机制三、整体架构与通信原理3.1 三个核心对象对象类作用服务器QTcpServer监听端口listen()后等待客户端连接服务端连接套接字QTcpSocket每个客户端对应一个用于收发数据客户端套接字QTcpSocket主动connectToHost()连接服务器3.2 通信流程时序客户端 服务端 | | |------ connectToHost() ----| (1) 发起连接 | | listen() 已就绪newConnection 触发 | | nextPendingConnection() 取出连接套接字 |------ connected 信号 -----| (2) 连接建立 | | |------ write(hello\n) ---| (3) 发消息 | | readyRead 触发readLine() 读到hello\n | | ---- 转发给其他客户端群聊 |------ write(hello\n) ---| (4) 其他客户端 readyRead 触发读到hello\n | | |------ disconnectFromHost -| (5) 断开连接 | | disconnected 触发从列表移除并释放3.3 为什么消息末尾要加换行符\n服务端和客户端都使用canReadLine()readLine()按行读取。TCP 是字节流协议没有消息边界概念多次write()的数据可能粘在一起到达。约定每条消息以\n结尾接收方就能用canReadLine()判断缓冲区里是否已有完整的一行再用readLine()读出一整条消息从而正确切分消息。四、服务端详解4.1 文件结构QTcpServerpro/ ├── main.cpp 程序入口 ├── mainwindow.h 主窗口类声明 ├── mainwindow.cpp 主窗口类实现核心逻辑 ├── mainwindow.ui 界面布局 └── QTcpServerpro.pro 工程文件4.2 mainwindow.h 成员变量QTcpServer*tcpserver;// 服务器对象监听端口、接受连接QListQTcpSocket*clientList;// 已连接客户端列表支持多客户端的关键关键设计用QListQTcpSocket*保存每一个客户端连接。若像早期版本那样只用一个QTcpSocket*指针新客户端连入会覆盖旧指针旧连接就失联了。4.3 构造函数做了什么ui-setupUi(this);// 1. 构建界面ui-comboBoxIP-addItem(GetLocalIpAddress());// 2. 把本机 IP 填进下拉框tcpservernewQTcpServer(this);// 3. 创建服务器对象connect(tcpserver,QTcpServer::newConnection,this,MainWindow::newconnection);// 4. 绑定信号connect(ui-inputMsg,QLineEdit::returnPressed,this,MainWindow::on_send_btn_clicked);// 5. 回车发送4.4 核心槽函数逐个讲解1GetLocalIpAddress()—— 获取本机 IPv4步骤 1. QHostInfo::localHostName() 拿到主机名如 DESKTOP-XXX 2. QHostInfo::fromName(主机名) 解析出该主机的所有地址列表 3. 遍历地址列表找出 protocol()IPv4 的地址 4. toString() 转字符串返回2on_start_btn_clicked()—— 启动服务QHostAddressaddress(ip);// IP 字符串 → QHostAddressif(!tcpserver-listen(address,port)){// listen 失败返回 falseQMessageBox::critical(...);// 弹窗提示如端口被占用return;}要点listen()是异步的返回true只表示开始监听成功真正的客户端连接由newConnection信号通知。3newconnection()—— 接受新连接核心while(tcpserver-hasPendingConnections()){// 一次可能来多个连接QTcpSocket*sockettcpserver-nextPendingConnection();// 取出连接clientList.append(socket);// 加入列表管理// 为这个 socket 绑定三个信号connect(socket,QTcpSocket::disconnected,this,MainWindow::clientdisconnect);connect(socket,QTcpSocket::readyRead,this,MainWindow::socketreaddata);connect(socket,QOverload...::of(QAbstractSocket::error),this,MainWindow::socketError);}要点hasPendingConnections()/nextPendingConnection()用while循环是为了处理多个客户端几乎同时连入的情况。每个客户端 socket 的readyRead、disconnected、error都连接到同一个槽函数所以槽内必须用sender()区分是哪个客户端。4currentSocket()—— 定位信号来源returnqobject_castQTcpSocket*(sender());sender()返回发出当前正在处理的信号的那个对象再用qobject_cast安全转换成QTcpSocket*。5socketreaddata()—— 接收并转发群聊核心while(socket-canReadLine()){QByteArray linesocket-readLine();// 原样读出一行含 \nQString textQString::fromUtf8(line).trimmed();// 转文本显示ui-dispMsg-append([in] text);// 服务器界面显示for(QTcpSocket*client:clientList){// 转发给其他客户端if(client!socketclient-state()QAbstractSocket::ConnectedState){client-write(line);}}}要点readLine()返回的字节原样转发含\n客户端才能canReadLine()正确切分。client ! socket排除发送者本身发送者本地已显示[out]避免重复。用QString::fromUtf8()转码保证中文不乱码。6clientdisconnect()—— 客户端下线clientList.removeOne(socket);// 从列表移除socket-deleteLater();// 延迟删除释放资源为什么用deleteLater()而不是delete因为当前正在执行这个 socket 的信号处理流程直接delete可能导致后续代码访问已销毁对象而崩溃。deleteLater()会把删除推迟到事件循环安全时机执行。7on_send_btn_clicked()—— 服务器广播QByteArray datastrmsg.toUtf8();data.append(\n);// 同样加换行符for(QTcpSocket*socket:clientList)if(socket-state()ConnectedState)socket-write(data);// 广播给所有客户端8closeEvent()—— 优雅退出if(tcpserver-isListening())tcpserver-close();// 停止监听for(QTcpSocket*socket:clientList)if(socket-state()ConnectedState)socket-disconnectFromHost();// 断开所有客户端event-accept();五、客户端详解5.1 文件结构QTcpClientPros/ ├── main.cpp 程序入口 ├── mainwindow.h 主窗口类声明 ├── mainwindow.cpp 主窗口类实现 ├── mainwindow.ui 界面布局 └── QTcpClientPros.pro 工程文件5.2 构造函数一次连接、避免重复tcpclientnewQTcpSocket(this);connect(tcpclient,QTcpSocket::connected,this,MainWindow::onConnected);connect(tcpclient,QTcpSocket::disconnected,this,MainWindow::onDisconnected);connect(tcpclient,QTcpSocket::readyRead,this,MainWindow::onReadyRead);connect(tcpclient,QOverload...::of(QAbstractSocket::error),this,MainWindow::onSocketError);connect(ui-inputMsg,QLineEdit::returnPressed,this,MainWindow::on_sendMsgbtn_clicked);重要改进信号槽的连接放在构造函数中只连接一次。早期版本把connect写在on_ConnectBtn_clicked()里每点一次连接就重复绑定一次导致槽函数被调用多次。5.3 核心槽函数1on_ConnectBtn_clicked()—— 发起连接if(tcpclient-state()ConnectedState){/* 已连接则提示并返回 */}tcpclient-connectToHost(addr,port);// 异步连接2onConnected()/onDisconnected()分别由connected/disconnected信号触发显示服务器信息、更新按钮状态。3on_sendMsgbtn_clicked()—— 发送QByteArray datastrmsg.toUtf8();data.append(\n);tcpclient-write(data);4onReadyRead()—— 接收while(tcpclient-canReadLine())ui-dispMsg-append([in] tcpclient-readLine().trimmed());5onSocketError()—— 出错提示ui-dispMsg-append([错误] tcpclient-errorString());6closeEvent()—— 优雅退出if(tcpclient-state()ConnectedState){tcpclient-disconnectFromHost();tcpclient-waitForDisconnected(1000);// 最多等 1 秒确保数据冲刷}event-accept();六、群聊消息如何互通整个群聊的关键只有服务端socketreaddata()里那段转发循环客户端 Awrite(你好\n)服务端对应 socket 的readyRead触发readLine()读到你好\nfor循环把你好\n写给除 A 之外的所有客户端客户端 B、C 各自的readyRead触发readLine()读到你好\n并显示。这就是典型的“服务器中转”Server Relay群聊模型客户端之间不直接相连所有消息都经过服务器转发。七、编译与运行步骤7.1 编译打开 Qt Creator分别打开两个.pro工程确认.pro文件中有QT network点击构建CtrlB确保无编译错误。7.2 运行先启动服务端运行QTcpServerpro点启动服务界面显示监听地址和端口再启动客户端运行QTcpClientPros可同时开多个实例模拟多人客户端选择服务端显示的 IP端口保持一致点连接服务器连接成功后任意一端在输入框打字按回车或点发送按钮即可群聊。7.3 本机自测小技巧服务端和客户端都在同一台电脑上时IP 填127.0.0.1即可要模拟两个客户端在 Qt Creator 中多次运行客户端程序即可每次运行是一个独立进程。八、核心 Qt API 速查表QTcpServer服务器API说明bool listen(const QHostAddress address, quint16 port)开始监听指定地址和端口成功返回truebool isListening()是否正在监听bool hasPendingConnections()是否有待处理的连接QTcpSocket *nextPendingConnection()取出一个待处理连接返回已连接的 socketvoid close()停止监听QHostAddress serverAddress()监听地址quint16 serverPort()监听端口QString errorString()错误描述QTcpSocket通信套接字客户端与服务端通用API说明void connectToHost(const QString host, quint16 port)异步连接服务器void disconnectFromHost()断开连接SocketState state()当前状态如ConnectedStateqint64 write(const QByteArray data)发送数据bool canReadLine()缓冲区是否已有一整行QByteArray readLine()读出一行含\nQHostAddress peerAddress()对端 IPquint16 peerPort()对端端口bool waitForDisconnected(int msecs)阻塞等待断开最多 msecs 毫秒QString errorString()错误描述QTcpSocket 常用信号信号触发时机connected()连接建立成功disconnected()连接断开readyRead()有新数据到达可读error(QAbstractSocket::SocketError)发生错误QAbstractSocket::SocketState 枚举state()返回值值含义UnconnectedState未连接HostLookupState正在解析主机名ConnectingState正在连接ConnectedState已连接ClosingState正在关闭……其他辅助类API说明QHostInfo::localHostName()获取本机主机名QHostInfo::fromName(主机名)解析主机名为地址列表QHostAddress::toString()地址转字符串QHostAddress::protocol()返回协议IPv4/IPv6qobject_castT*(obj)安全类型转换失败返回nullptrsender()返回发出当前信号的对象指针QString::fromUtf8(bytes)UTF-8 字节转字符串中文不乱码信号槽两种写法对比// 旧式宏拼写错误只能在运行期发现connect(obj,SIGNAL(readyRead()),this,SLOT(onReadyRead()));// 新式函数指针编译期检查推荐connect(obj,QTcpSocket::readyRead,this,MainWindow::onReadyRead);// 处理重载信号error 与 error() 函数同名connect(obj,QOverloadQAbstractSocket::SocketError::of(QAbstractSocket::error),this,MainWindow::onSocketError);九、常见问题排查现象可能原因解决服务端启动服务报错端口被占用换一个端口号客户端连接不上IP/端口填错、服务端未启动核对服务端显示的地址和端口中文乱码收发编码不一致统一用toUtf8()/fromUtf8()消息粘在一起/截断没按行协议收发发送端统一加\n接收端用readLine()多客户端时后连的覆盖先连的用单指针保存连接改用QListQTcpSocket*断开客户端后程序崩溃信号处理中直接delete改用deleteLater()槽函数被重复调用信号重复 connect把 connect 移到构造函数只做一次编译报QCloseEvent不完整缺头文件#include QCloseEvent学习建议先对照客户端mainwindow.cpp的头部注释理解整体执行流程再看服务端的socketreaddata()体会服务器中转的群聊本质最后跑起来用两个客户端互相发消息验证。