TCP 聊天程序说明文档
本文档详细讲解
QTcpServerpro(服务端)与QTcpClientPros(客户端)两个 Qt 项目的实现原理、运行步骤、核心 API 与完整代码流程,适合逐行对照源码仔细琢磨。


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