QT_实战TCP 聊天程序说明文档以及实现

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() —— 优雅退出)
    • 五、客户端详解
      • [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() —— 优雅退出)
    • 六、群聊消息如何互通
    • 七、编译与运行步骤
      • [7.1 编译](#7.1 编译)
      • [7.2 运行](#7.2 运行)
      • [7.3 本机自测小技巧](#7.3 本机自测小技巧)
    • [八、核心 Qt API 速查表](#八、核心 Qt API 速查表)
    • 九、常见问题排查

一、项目概述

项目 角色 职责
QTcpServerpro 服务端 监听端口、接受多个客户端连接、接收并转发消息(群聊中心)
QTcpClientPros 客户端 连接服务器、发送消息、接收服务器转发来的消息

最终效果 :服务端启动后,多个客户端连接上来,任意一个客户端发的消息,其余所有客户端(以及服务器)都能看到,实现一个简易的 TCP 群聊系统


二、开发环境

  • 开发框架:Qt 5.7 及以上(本机为 Qt 5.12.8 + MinGW)
  • 必需模块:coreguinetwork.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 的 readyReaddisconnectederror 都连接到同一个 槽函数,所以槽内必须用 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() 里那段转发循环

  1. 客户端 A write("你好\n")
  2. 服务端对应 socket 的 readyRead 触发;
  3. readLine() 读到 "你好\n"
  4. for 循环把 "你好\n" 写给除 A 之外的所有客户端;
  5. 客户端 B、C 各自的 readyRead 触发,readLine() 读到 "你好\n" 并显示。

这就是典型的 "服务器中转"(Server Relay) 群聊模型:客户端之间不直接相连,所有消息都经过服务器转发。


七、编译与运行步骤

7.1 编译

  1. 打开 Qt Creator,分别打开两个 .pro 工程;
  2. 确认 .pro 文件中有 QT += network
  3. 点击构建(Ctrl+B),确保无编译错误。

7.2 运行

  1. 先启动服务端 :运行 QTcpServerpro,点"启动服务",界面显示监听地址和端口;
  2. 再启动客户端 :运行 QTcpClientPros(可同时开多个实例模拟多人);
  3. 客户端选择服务端显示的 IP,端口保持一致,点"连接服务器";
  4. 连接成功后,任意一端在输入框打字按回车(或点发送按钮)即可群聊。

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() 体会"服务器中转"的群聊本质,最后跑起来用两个客户端互相发消息验证。

相关推荐
01_ice1 小时前
MySQL基本查询1
数据库·mysql
腾科IT教育2 小时前
Oracle OCP/OCM认证含金量实测2026版
数据库·oracle·ocp·ocm·甲骨文
SoStraw2 小时前
webrpc 常见问题 FAQ:无公网 IP、Token、传文件,以及和 frp / WebRTC 的差别
tcp/ip·webrtc·frp·token·faq·webrpc·无公网ip
不可求~10 小时前
多模型路由怎么落地:把任务分流写进项目的最小实现
java·前端·数据库
Lumistory11 小时前
城市建筑照明的落地与长效运营观察
大数据·数据库·中间件·光照贴图
zhao32668575111 小时前
静态IP代理固定IP与动态IP的根本差异,账号操作安全性如何体现
网络·网络协议·tcp/ip
围炉聊科技13 小时前
长期免费的数据库方案——零成本基建系列
数据库
AC赳赳老秦13 小时前
公开图片 OCR 数据提取:OpenClaw 合规采集公开信息图,识别文字与表格并转化为结构化数据
java·大数据·前端·数据库·python·php·openclaw
byxdaz14 小时前
QT中服务使用
开发语言·qt