【Qt实战】心跳检测与粘包处理:打造稳定可靠的TCP Socket通信
引言
【上篇我们已经介绍了怎么实现tcpt_socket通信,今天我们重点解决心跳及黏包问题,本项目原码以上传至git(分了两版本,第一版本粗略实现通信,第二版在第一版基础上解决了心跳黏包)】
在网络编程中,TCP Socket通信是最基础也是最重要的技术之一。然而,实际开发中我们会遇到两个经典难题:粘包问题 和连接保活问题。本文将结合一个完整的Qt聊天程序项目,深入讲解如何解决这些问题。
一、TCP粘包问题的本质
为什么会有粘包问题?
TCP是面向流的协议,它不关心消息边界,只保证数据的有序到达。这就好比自来水管,我们倒入一杯水,再倒入一杯水,接水的人可能一次接到两杯水(粘包),也可能分两次接到(拆包)。
cpp
// 发送端连续发送两个包
send("Hello");
send("World");
// 接收端可能收到:
// "HelloWorld" (粘包)
// "He" + "lloWorld" (拆包)
我们的解决方案:长度前缀法
在项目中,我们为每个数据包添加了一个头部,包含包的类型和长度信息:
cpp
// common.h
struct Head
{
int type; // 区分不同功能类型
int len; // 整个包的长度(解决粘包问题)
int version; // 区分不同版本
Head() {
version = 10; // 表示1.0版本
}
};
// 所有具体结构体都继承Head
struct ChatReq : public Head
{
char user_name[32]; // 用户名
char content[100]; // 内容
ChatReq(){
type = CHAT_REQ;
len = sizeof(ChatReq); // 关键:记录完整长度
}
};
粘包处理的核心代码
cpp
// 服务端 myRead() 函数中的粘包处理逻辑
void ServerMainWindow::myRead()
{
QTcpSocket *client = (QTcpSocket*)sender();
QByteArray buffer = client->readAll();
// 将数据追加到该客户端的缓冲区
QByteArray &clientBuffer = m_clientBuffers[client];
clientBuffer.append(buffer);
int len = clientBuffer.size();
while (len > 0) {
// 步骤1:检查是否足够读取数据头
if (len < sizeof(Head)) {
break; // 包头还没收完,等待下次
}
// 步骤2:通过数据头获取总长度
Head* head = (Head*)clientBuffer.data();
int total_len = head->len;
// 步骤3:检查是否足够读取整个包
if (len < total_len) {
break; // 整个包还没收完
}
// 步骤4:提取一个完整的包
QByteArray packet = clientBuffer.left(total_len);
// 步骤5:解包处理(通过信号槽异步处理)
emit unpackSignal(client, packet);
// 步骤6:移除已处理的包
clientBuffer = clientBuffer.mid(total_len);
len = clientBuffer.size();
}
}
关键点:每个客户端都有自己的缓冲区,独立处理粘包问题。
二、心跳检测机制详解
为什么需要心跳检测?
- 检测连接状态:网络可能随时中断,但TCP连接不会立即断开
- 清理僵尸连接:客户端异常退出时,释放服务器资源
- 实现断线重连:客户端自动检测并重连
- 避免TCP Keep-Alive的缺陷:默认2小时才检测,不满足实时应用需求
心跳参数设计
cpp
#define HEART_CHECK_TIMES 5 // 心跳检测阈值
// 服务器端:每10秒检查一次,阈值5 → 50秒超时
// 客户端:每10秒发送一次,等待3秒响应 → 15秒超时
服务端心跳检测实现
cpp
// 服务端心跳检测逻辑
void ServerMainWindow::checkHeartbeat()
{
QList<QTcpSocket*> toRemove; // 需要移除的客户端列表
// 遍历所有客户端
for (auto it = m_lastHeartTime.begin(); it != m_lastHeartTime.end(); ++it) {
QTcpSocket* client = it.key();
// 递减心跳阈值
if (m_heartThresholds.contains(client)) {
m_heartThresholds[client]--;
qDebug() << "心跳超时计数:" << m_heartThresholds[client];
// 检查是否超时
if (m_heartThresholds[client] <= 0) {
qDebug() << "客户端心跳超时:" << client;
toRemove.append(client);
}
}
}
// 断开超时的客户端
for (QTcpSocket* client : toRemove) {
if (client->state() == QAbstractSocket::ConnectedState) {
client->close(); // 断开连接
}
}
}
工作原理:
- 每个客户端初始阈值 = 5
- 每10秒检查一次,阈值-1
- 收到数据包时,阈值重置为5
- 阈值降为0时,认为客户端已掉线,断开连接
- 总超时时间 = 10秒 × 5 = 50秒
客户端心跳机制
cpp
// 客户端发送心跳
void ClientMainWindow::heartBeatSlot()
{
if (m_client && m_connected && m_isLogin) {
HeartCheckReq req;
m_client->write((char*)&req, req.len);
qDebug() << "发送心跳请求";
// 启动检查定时器,3秒后检查响应
m_checkTimer->start(3000);
}
}
// 检查服务器响应
void ClientMainWindow::checkServerResponse()
{
m_heartTimeoutCount++;
qDebug() << "心跳超时计数:" << m_heartTimeoutCount;
if (m_heartTimeoutCount >= HEART_CHECK_TIMES) {
// 服务器无响应,断开连接
m_client->disconnectFromHost();
qDebug() << "服务器无响应,断开连接";
// 重置状态
m_heartTimeoutCount = 0;
m_connected = false;
m_isLogin = false;
// 更新界面
ui->pushButton->setEnabled(false);
ui->pushButton_login->setText("登录");
} else {
// 继续等待响应
m_checkTimer->start(3000);
}
}
客户端逻辑:
- 每10秒发送一次心跳
- 等待3秒服务器响应
- 连续5次无响应(15秒)
- 认为服务器掉线繁忙,则断开连接
三、项目架构与设计模式
信号槽机制的应用
本项目巧妙运用Qt的信号槽机制实现异步解包:
cpp
// 连接信号与槽
connect(this, SIGNAL(unpackSignal(QTcpSocket*, QByteArray)),
this, SLOT(unpackSlot(QTcpSocket*, QByteArray)));
// 在myRead中发射信号
emit unpackSignal(client, packet);
// 在unpackSlot中处理具体业务
void ServerMainWindow::unpackSlot(QTcpSocket *client, QByteArray buffer)
{
int type = *(int*)buffer.data();
switch(type) {
case CHAT_REQ: // 处理聊天
case LOGIN_REQ: // 处理登录
// ... 其他类型
}
}
优势:
- 解耦网络接收和业务处理
- 便于扩展到多线程
- 代码结构清晰,易于维护
客户端状态管理
客户端通过三个状态变量管理连接状态:
cpp
private:
bool m_isLogin; // 登录状态
bool m_connected; // 连接状态
int m_heartTimeoutCount; // 心跳超时计数
状态转换逻辑:
- 连接成功 →
m_connected = true - 登录成功 →
m_isLogin = true - 心跳超时 → 断开连接 → 所有状态重置
- 手动注销 → 断开连接 → 所有状态重置
四、关键代码解析
1. 登录逻辑优化
cpp
void ClientMainWindow::on_pushButton_login_clicked()
{
if(ui->pushButton_login->text()=="注销"){
// 注销操作
m_isLogin = false;
m_client->disconnectFromHost();
// ... 状态重置
}else{
// 登录操作:先检查连接状态
if (m_client->state() != QAbstractSocket::ConnectedState) {
m_client->connectToHost("127.0.0.1", 10086);
}
// 发送登录请求
LoginReq req;
strcpy(req.user_name, ui->lineEdit_userName->text().toLocal8Bit().data());
m_client->write((char*)(&req), req.len);
}
}
改进点:登录前检查连接状态,未连接则先连接,提高用户体验。
2. 聊天消息的双向确认
cpp
// 服务端处理聊天请求
void ServerMainWindow::unpackSlot(QTcpSocket *client, QByteArray buffer)
{
if(type == CHAT_REQ){
ChatReq *req = (ChatReq*)buffer.data();
// ✅ 修复:给发送者回复确认
ChatRes res;
res.type = CHAT_RES;
res.len = sizeof(ChatRes);
client->write((char*)&res, res.len);
// 转发给其他客户端
for(int i=0; i < m_clients.size(); ++i){
if(client != m_clients.at(i)){
m_clients.at(i)->write((char*)req, req->len);
}
}
}
}
意义:确保消息发送成功,提供更好的用户体验。
3. 资源管理
cpp
void ServerMainWindow::clientDisconnected()
{
QTcpSocket *client = qobject_cast<QTcpSocket*>(sender());
if(!client) return;
// 从所有容器中移除
m_clients.removeOne(client);
m_clientNames.remove(client);
m_lastHeartTime.remove(client);
m_heartThresholds.remove(client);
m_clientBuffers.remove(client);
// 更新界面
ui->label->setText(QString("在线 %1").arg(m_clients.size()));
}
重要性:防止内存泄漏,确保服务器长期稳定运行。
五、遇到的问题与解决方案
问题1:心跳检测误判
现象:网络波动时,客户端被误判为掉线
解决方案:
- 设置合理的超时阈值(15秒)
- 使用阈值递减而非固定超时
- 收到任何数据包都重置心跳计数
问题2:内存泄漏
现象:长时间运行后内存占用不断增加
解决方案:
- 使用
QMap<QTcpSocket*, QByteArray>为每个客户端维护独立缓冲区 - 在
clientDisconnected()中清理所有相关资源 - 使用
free()释放动态分配的柔性数组内存
问题3:界面卡顿
现象:大量消息时界面响应变慢
解决方案:
- 使用信号槽异步处理网络数据
- 界面更新放在主线程
- 网络收发放在单独的循环中
六、性能优化建议
1. 缓冲区优化
cpp
// 预分配缓冲区,减少内存碎片
QByteArray buffer;
buffer.reserve(4096); // 预分配4KB空间
2. 连接数限制
cpp
// 防止DDoS攻击
void ServerMainWindow::acceptNewClient()
{
if(m_clients.size() >= MAX_CLIENTS) {
QTcpSocket *client = m_server->nextPendingConnection();
client->close();
client->deleteLater();
return;
}
// ... 正常处理
}
3. 数据包验证
cpp
// 防止恶意数据包
void ServerMainWindow::myRead()
{
// ... 读取数据头
Head* head = (Head*)clientBuffer.data();
// 验证包长度合法性
if(head->len <= 0 || head->len > MAX_PACKET_SIZE) {
qDebug() << "非法数据包长度,断开连接";
client->disconnectFromHost();
return;
}
// ... 继续处理
}
七、项目实例
实现效果:
初始态:
登录后:
心跳检测:
由于test3到达计数阈值时还没发送任何消息,故被断开连接
示例代码:
cpp
//公共头文件
#ifndef COMMON_H
#define COMMON_H
// 数据包头
struct Head
{
int type; // 区分不同功能类型
int len; // 表示整个封包的长度,是为了处理粘包问题
int version; // 区分不同版本
Head() {
version = 10; // 表示1.0版本
}
};
enum TypeInfo
{
CHAT_REQ = 100, // 聊天请求
CHAT_RES, // 聊天响应
LOGIN_REQ, // 登录请求
LOGIN_RES, // 登录响应
SEARCH_NAME_REQ, // 查询名字请求
SEARCH_NAME_RES, // 查询名字响应
HEART_CHECK_REQ, //心跳检测请求
HEART_CHECK_RES //心跳检测响应
};
// 聊天
struct ChatReq : public Head
{
char user_name[32]; // 用户名
char content[100]; // 内容
ChatReq(){
type = CHAT_REQ;
len = sizeof(ChatReq);
}
};
struct ChatRes : public Head
{
int status; // 状态 0=失败,1=成功
ChatRes(){
type = CHAT_RES;
len = sizeof(ChatRes);
version = 10;
status = 1;
}
};
// 登录
struct LoginReq : public Head
{
char user_name[32];
LoginReq() {
type = LOGIN_REQ;
len = sizeof(LoginReq);
version=10;
}
};
struct LoginRes : public Head
{
int status; // 登录状态
LoginRes() {
type = LOGIN_RES;
len = sizeof(LoginRes);
version = 10;
status = 1;
}
};
// 查询在线用户
struct SearchNameReq : public Head
{
SearchNameReq() {
type = SEARCH_NAME_REQ;
len = sizeof(SearchNameReq);
version = 10;
}
};
struct UserInfo
{
char user_name[32];
};
struct SearchNameRes : public Head
{
int cnt; // 用户数量
UserInfo info[0]; // 柔性数组
SearchNameRes() {
type = SEARCH_NAME_RES;
len = sizeof(SearchNameRes); // 不是真正的长度
version=10;
cnt=0;
}
};
// 心跳检测
struct HeartCheckReq : public Head
{
HeartCheckReq() {
type=HEART_CHECK_REQ;
len=sizeof(HeartCheckReq);
version=10;
}
};
struct HeartCheckRes : public Head
{
HeartCheckRes() {
type=HEART_CHECK_RES;
len=sizeof(HeartCheckRes);
version=10;
}
};
#define HEART_CHECK_TIMES 5 // 心跳检测阈值
#endif // COMMON_H
cpp
//客户端源文件
#include "clientmainwindow.h"
#include "ui_clientmainwindow.h"
#include <QDebug>
ClientMainWindow::ClientMainWindow(QWidget *parent) :
QMainWindow(parent),
m_isLogin(false),
m_connected(false),
m_heartTimeoutCount(0),
ui(new Ui::ClientMainWindow)
{
ui->setupUi(this);
m_client = new QTcpSocket(this);
m_client->connectToHost("127.0.0.1",10086);
// 如果连接成功
connect(m_client, &QTcpSocket::connected, this, [this]() {
qDebug() << "conn ok";
m_connected = true;
});
// 如果连接断开
// connect(m_client, 断开信号, this, [this]() {
// m_client->disconnectFromHost();
// m_heartTimeoutCount = 0;
// qDebug()<<"conn fail"<<m_client->errorString();
// qDebug() << "服务器无响应,断开连接";
// m_connected = false;
// m_isLogin = false;
// ui->pushButton->setEnabled(false);
// ui->pushButton_login->setText("登录");
// // 新增:停止所有定时器
// if (m_heartTimer) m_heartTimer->stop();
// if (m_timer) m_timer->stop();
// // 新增:清空缓冲区
// m_buffer.clear();
// });
// 连接收包的槽
connect(m_client, &QTcpSocket::readyRead, this, &ClientMainWindow::myRead);
connect(this,SIGNAL(unpackSignal(QByteArray)),this,SLOT(unpackSlot(QByteArray))); //解包
// 定时器
m_timer=new QTimer(this);//用户列表定时器初始化
connect(m_timer,&QTimer::timeout,this,&ClientMainWindow::searchNameSlot);//一到时间就去检查用户列表
m_heartTimer = new QTimer(this);//心跳定时器初始化
connect(m_heartTimer, &QTimer::timeout, this, &ClientMainWindow::heartBeatSlot);
m_checkTimer = new QTimer(this);//检查服务器响应定时器初始化
connect(m_checkTimer, &QTimer::timeout, this, &ClientMainWindow::checkServerResponse);
// 当客户端发送心跳包时,就启动检查定时器(查询服务器是否回包),如果超时(3s)没回包就增加加超时计数HEART_CHECK_TIMES,当达到HEART_CHECK_TIMES=5的阈值时,因为服务器无响应就断卡连接
m_checkTimer->setSingleShot(true);
}
ClientMainWindow::~ClientMainWindow()
{
delete ui;
}
void ClientMainWindow::on_pushButton_clicked()
{
if(m_client){
QString str = ui->lineEdit_chat->text();
QString userName = ui->lineEdit_userName->text();
// 新增:检查登录状态
if (!m_isLogin) {
qDebug() << "请先登录";
return;
}
ChatReq req;
strcpy(req.content,str.toLocal8Bit().data());
strcpy(req.user_name,userName.toLocal8Bit().data());
qDebug()<<"len:"<<m_client->write((char*)&req,req.len);
ui->textBrowser->append(userName+":"+str);
}
}
// 修改:重写myRead函数,添加粘包处理
void ClientMainWindow::myRead()
{
QByteArray buffer = m_client->readAll();
qDebug()<<"buffer:"<<buffer;
m_buffer.append(buffer); // 新增:追加到缓冲区
int len = m_buffer.size();
/*
qDebug()<<"buffer data:"<<buffer.data();
qDebug()<<"buffer有中文:"<<QString::fromLocal8Bit(buffer);
*/
while (len > 0) {
// 新增:检查是否足够读取数据头
if (len < sizeof(Head)) {// 和数据头长度对比,小于,说明包头还没收完,继续等下次
break; // 数据头还没收完
}
// 新增:通过数据头取出总长度
Head* head = (Head*)m_buffer.data();
int total_len = head->len;
// 新增:再次比较封包总长度够不够
if (len < total_len) {
break; // 整个包还没收完
}
// 够了根据类型来进行解包处理
// 把当前够的这个包,拿去解开并操作
QByteArray packet = m_buffer.left(total_len);// 从左边0下标开始截取指定长度内容
// ========相当于调用了解包函数,通过发信号,可用来指定同步或异步处理========
emit unpackSignal(packet);
// 截取剩余长度的内容,继续处理剩下的
// 新增:移除已处理的包
m_buffer = m_buffer.mid(total_len);//从中间指定位置往后截取
len = m_buffer.size();
}
}
// 解包槽函数
void ClientMainWindow::unpackSlot(QByteArray buffer)
{
int type = *(int*)buffer.data();
if(type == CHAT_REQ){//注意应该是接收到转发请求则打印到公屏,因为CHAT_RES结构体没有内容
ChatReq *req = (ChatReq*)buffer.data();
ui->textBrowser->append(QString("%1:%2").arg(QString::fromLocal8Bit(req->user_name)).arg(QString::fromLocal8Bit(req->content)));
// 新增:重置心跳超时计数
m_heartTimeoutCount = 0;
}else if(type==LOGIN_RES){
qDebug()<<"收到登录的响应";
LoginRes *res=(LoginRes*)buffer.data();
if(res->status==1){// 成功,使能发送按钮,可正常聊天
m_isLogin = true; // 新增:设置登录状态
ui->pushButton->setEnabled(true);
ui->pushButton_login->setText("注销");
qDebug()<<"登录成功";
ui->listWidget->addItem(ui->lineEdit_userName->text());
// 新增:启动定时器
m_heartTimer->start(10000); // 10秒发送一次心跳
m_timer->start(5000); // 启动用户列表查询定时器
}
}else if(type==SEARCH_NAME_RES){
qDebug()<<"收到查询在线用户的响应";
SearchNameRes *res=(SearchNameRes*)buffer.data();
ui->listWidget->clear();
for(int i=0;i<res->cnt;i++)ui->listWidget->addItem(QString::fromLocal8Bit(res->info[i].user_name));
}else if(type==HEART_CHECK_RES){// 新增:处理心跳响应
qDebug()<<"收到心跳响应";
m_heartTimeoutCount = 0; // 重置超时计数
m_checkTimer->stop(); // 停止检查定时器
}
}
void ClientMainWindow::on_pushButton_login_clicked()
{
if(ui->pushButton_login->text()=="注销"){
// 修改:改为断开连接而不是关闭窗口
m_isLogin = false;
m_client->disconnectFromHost();
ui->pushButton->setEnabled(false);
ui->pushButton_login->setText("登录");
// 新增:停止定时器
if (m_heartTimer) m_heartTimer->stop();
if (m_timer) m_timer->stop();
}else{
// ********** 先检查连接状态,如果没有连接,则先连接 **********
if (m_client->state() != QAbstractSocket::ConnectedState) m_client->connectToHost("127.0.0.1", 10086);
LoginReq req;
strcpy(req.user_name, ui->lineEdit_userName->text().toLocal8Bit().data());
qDebug() << "登录len:" << m_client->write((char*)(&req), req.len);
}
}
void ClientMainWindow::searchNameSlot(){
// 新增:检查登录状态
if (m_client && m_isLogin) {
SearchNameReq req;
if(m_client)qDebug()<<"查询用户:"<<m_client->write((char*)&req,req.len);
}
}
// 新增:心跳检测槽函数(用于检测服务器是否断联)
void ClientMainWindow::heartBeatSlot()
{
if (m_client && m_connected && m_isLogin) {
HeartCheckReq req;
m_client->write((char*)&req, req.len);
qDebug() << "发送心跳请求";
// 新增:启动检查定时器
m_checkTimer->start(3000); // 3秒后检查响应
}
}
// 新增:检查服务器响应函数
void ClientMainWindow::checkServerResponse()
{
m_heartTimeoutCount++;
qDebug() << "心跳超时计数:" << m_heartTimeoutCount;
if (m_heartTimeoutCount >= HEART_CHECK_TIMES) {
//(也可以emit 断开信号),由信号槽处理断开连接
m_client->disconnectFromHost();
m_heartTimeoutCount = 0;
qDebug()<<"conn fail"<<m_client->errorString();
qDebug() << "服务器无响应,断开连接";
m_connected = false;
m_isLogin = false;
ui->pushButton->setEnabled(false);
ui->pushButton_login->setText("登录");
// 新增:停止所有定时器
if (m_heartTimer) m_heartTimer->stop();
if (m_timer) m_timer->stop();
// 新增:清空缓冲区
m_buffer.clear();
} else {
// 继续等待响应
m_checkTimer->start(3000);
}
}
cpp
//服务端源文件
#include "servermainwindow.h"
#include "ui_servermainwindow.h"
#include <QDebug>
#include <QtGlobal>
ServerMainWindow::ServerMainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::ServerMainWindow)
{
ui->setupUi(this);
m_server = new QTcpServer(this);
m_server->listen(QHostAddress::Any,10086);
if(m_server->isListening()){
qDebug()<<"服务器启动成功";
connect(m_server,&QTcpServer::newConnection,this,&ServerMainWindow::acceptNewClient);
}
connect(this,SIGNAL(unpackSignal(QTcpSocket*,QByteArray)),this,SLOT(unpackSlot(QTcpSocket*,QByteArray))); //解包
// 新增:心跳检查定时器
m_heartCheckTimer = new QTimer(this);
connect(m_heartCheckTimer, &QTimer::timeout, this, &ServerMainWindow::checkHeartbeat);
m_heartCheckTimer->start(10000); // 10秒检查一次
}
ServerMainWindow::~ServerMainWindow()
{
delete ui;
}
void ServerMainWindow::acceptNewClient()
{
QTcpSocket *client = m_server->nextPendingConnection();
qDebug()<<"client:"<<client;
connect(client,&QTcpSocket::readyRead,this,&ServerMainWindow::myRead);
// 保存套接字
m_clients.append(client);
// 新增:初始化客户端信息
m_lastHeartTime[client] = QDateTime::currentDateTime(); // 记录心跳时间
m_heartThresholds[client] = HEART_CHECK_TIMES; // 设置心跳阈值
m_clientBuffers[client] = QByteArray(); // 初始化粘包缓冲区
qDebug()<<"client size:"<<m_clients.size();
ui->label->setText(QString("在线 %1人").arg(m_clients.size()));
connect(client,&QTcpSocket::disconnected,this,&ServerMainWindow::clientDisconnected);
}
// 修改:重写myRead函数,添加粘包处理
void ServerMainWindow::myRead()
{
QTcpSocket *client = (QTcpSocket*)sender();
if(!client) return;
QByteArray buffer = client->readAll();
qDebug()<<"buffer:"<<buffer;
QByteArray &clientBuffer = m_clientBuffers[client]; // 获取该客户端的缓冲区
clientBuffer.append(buffer); // 追加到缓冲区
int len = clientBuffer.size();
while (len > 0) {
// 新增:检查是否足够读取数据头
if (len < sizeof(Head)) {
break; // 数据头还没收完
}
// 新增:通过数据头取出总长度
Head* head = (Head*)clientBuffer.data();
int total_len = head->len;
// 新增:检查是否足够读取整个包
if (len < total_len) {
break; // 整个包还没收完
}
// 新增:提取一个完整的包
QByteArray packet = clientBuffer.left(total_len);
// ========相当于调用了解包函数,通过发信号,可用来指定同步或异步处理========
emit unpackSignal(client,packet);
// 新增:移除已处理的包
clientBuffer = clientBuffer.mid(total_len);
len = clientBuffer.size();
}
}
void ServerMainWindow::clientDisconnected()
{
QTcpSocket *client=qobject_cast<QTcpSocket*>(sender());
if(!client) return;
qDebug() << "客户端断开:" << client;
// 释放关闭的套接字
if(m_clients.removeOne(client)){
ui->label->setText(QString("在线 %1").arg(m_clients.size()));
}
if(m_clientNames.count(client)==1)// 存在
{
// QString userName = m_clientNames.value(client);
m_clientNames.remove(client);
ui->listWidget->clear();
for(QString e:m_clientNames.values()){
ui->listWidget->addItem(e);
}
}
// 新增:清理客户端信息
m_lastHeartTime.remove(client);
m_heartThresholds.remove(client);
m_clientBuffers.remove(client);
}
void ServerMainWindow::unpackSlot(QTcpSocket *client,QByteArray buffer)
{
// 解包
int type = *(int*)buffer.data();
if(type == CHAT_REQ){
// 发送转发请求给其他客户端
qDebug()<<"收到聊天请求";
ChatReq *req = (ChatReq*)buffer.data();
// ✅ 修复:给发送者回复确认
ChatRes res;
res.type = CHAT_RES;
res.len = sizeof(ChatRes);
client->write((char*)&res, res.len);
// 给所有在线客户端转发
for(int i=0;i < m_clients.size();++i){
if(client != m_clients.at(i)){
qDebug()<<"i:"<<i<<m_clients.at(i)->write((char*)req,req->len);
}
}
// 新增:更新心跳时间
m_lastHeartTime[client] = QDateTime::currentDateTime();
m_heartThresholds[client] = HEART_CHECK_TIMES; // 重置阈值
}else if(type==LOGIN_REQ){
LoginReq *req=(LoginReq*)(buffer.data());
ui->listWidget->addItem(QString::fromLocal8Bit(req->user_name));
m_clientNames[client]=QString::fromLocal8Bit(req->user_name);
//回个响应包
LoginRes res;
res.status=1;
client->write((char*)&res,res.len);
}else if(type==SEARCH_NAME_REQ){
int cnt=m_clientNames.size();
int len_total=sizeof(SearchNameRes)+cnt*sizeof(UserInfo);
SearchNameRes *res=(SearchNameRes*)malloc(len_total);
res->type=SEARCH_NAME_RES;
res->len=len_total;
res->cnt=cnt;
for(int i=0;i<cnt;i++){
strcpy(res->info[i].user_name,m_clientNames.values().at(i).toLocal8Bit().data());
}
client->write((char*)res,res->len);
free(res);
}else if(type==HEART_CHECK_REQ){ // 新增:处理心跳请求
qDebug() << "收到心跳请求";
HeartCheckRes res;
client->write((char*)&res, res.len);
}
}
void ServerMainWindow::checkHeartbeat()
{
QList<QTcpSocket*> toRemove;//移除队列
qDebug() << "=== 开始心跳检查 ===当前客户端数量:"<< m_clients.size();
// 遍历所有客户端
for (auto it = m_lastHeartTime.begin(); it != m_lastHeartTime.end(); ++it) {
QTcpSocket* client = it.key();
// 递减心跳阈值
if (m_heartThresholds.contains(client)) {
m_heartThresholds[client]--;
qDebug()<<"心跳超时计数:"<<m_heartThresholds[client];
// 检查是否超时
if (m_heartThresholds[client] <= 0) {
qDebug() << "客户端心跳超时:" << client;
toRemove.append(client);
}
}
}
// 断开超时的客户端
for (QTcpSocket* client : toRemove) {
if (client->state() == QAbstractSocket::ConnectedState) {
m_heartThresholds.remove(client);
client->close();//闭套接字的I/O设备,并调用disconnectFromHost()以断开套接字的连接
}
}
}
八、总结与展望
通过本项目,我们实现了:
✅ 已实现功能
- 完整的TCP通信框架:支持多客户端并发
- 粘包处理机制:使用长度前缀法,稳定可靠
- 心跳检测系统:双向检测
- 资源管理:完善的连接管理和内存管理
- 友好界面:实时显示在线用户和聊天记录
🔮 扩展方向
- 加密传输:添加SSL/TLS支持
- 文件传输:支持大文件分片传输
- 群组聊天:创建聊天室和群组
- 消息存储:数据库支持消息历史
- 跨平台:适配Android/iOS移动端
📚 核心知识点
- TCP粘包问题的本质与解决方案
- 心跳检测的原理与实现
- Qt信号槽在异步编程中的应用
- 网络编程中的资源管理
- 客户端状态机设计
源码获取
git@gitee.com:HANDM-735/tcp_socket.git
完整项目代码已开源,包含详细注释
项目结构:
├── common.h # 公共数据结构和协议定义
├── servermainwindow.h/cpp # 服务器端实现
├── clientmainwindow.h/cpp # 客户端实现
└── README.md # 项目说明文档
通过这个项目,你不仅学会了如何解决TCP通信中的粘包和心跳检测问题,还掌握了Qt网络编程的核心技巧。希望本文对你的学习和开发有所帮助!



