【Qt】心跳检测与粘包处理:打造稳定可靠的TCP Socket通信

【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();
    }
}

关键点:每个客户端都有自己的缓冲区,独立处理粘包问题。

二、心跳检测机制详解

为什么需要心跳检测?

  1. 检测连接状态:网络可能随时中断,但TCP连接不会立即断开
  2. 清理僵尸连接:客户端异常退出时,释放服务器资源
  3. 实现断线重连:客户端自动检测并重连
  4. 避免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; // 心跳超时计数

状态转换逻辑

  1. 连接成功 → m_connected = true
  2. 登录成功 → m_isLogin = true
  3. 心跳超时 → 断开连接 → 所有状态重置
  4. 手动注销 → 断开连接 → 所有状态重置

四、关键代码解析

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()以断开套接字的连接
        }
    }
}

八、总结与展望

通过本项目,我们实现了:

✅ 已实现功能

  1. 完整的TCP通信框架:支持多客户端并发
  2. 粘包处理机制:使用长度前缀法,稳定可靠
  3. 心跳检测系统:双向检测
  4. 资源管理:完善的连接管理和内存管理
  5. 友好界面:实时显示在线用户和聊天记录

🔮 扩展方向

  1. 加密传输:添加SSL/TLS支持
  2. 文件传输:支持大文件分片传输
  3. 群组聊天:创建聊天室和群组
  4. 消息存储:数据库支持消息历史
  5. 跨平台:适配Android/iOS移动端

📚 核心知识点

  1. TCP粘包问题的本质与解决方案
  2. 心跳检测的原理与实现
  3. Qt信号槽在异步编程中的应用
  4. 网络编程中的资源管理
  5. 客户端状态机设计

源码获取

git@gitee.com:HANDM-735/tcp_socket.git

完整项目代码已开源,包含详细注释

复制代码
项目结构:
├── common.h                 # 公共数据结构和协议定义
├── servermainwindow.h/cpp  # 服务器端实现
├── clientmainwindow.h/cpp  # 客户端实现
└── README.md               # 项目说明文档

通过这个项目,你不仅学会了如何解决TCP通信中的粘包和心跳检测问题,还掌握了Qt网络编程的核心技巧。希望本文对你的学习和开发有所帮助!


相关推荐
a程序小傲4 小时前
淘宝Java面试被问:Atomic原子类的实现原理
java·开发语言·后端·面试
laocooon5238578864 小时前
C++中的安全指针(智能指针)
开发语言·c++
咸鱼加辣4 小时前
【python面试题】LRUCache
开发语言·python
LitchiCheng4 小时前
WSL2 中 pynput 无法捕获按键输入?
开发语言·python
中年程序员一枚4 小时前
Python 中处理视频添加 / 替换音频
开发语言·python·音视频
yuuki2332334 小时前
【C++】模板初阶
java·开发语言·c++
qq_12498707534 小时前
基于Spring Boot的社区医院管理系统的设计与实现(源码+论文+部署+安装)
java·数据库·人工智能·spring boot·毕业设计
爱吃大芒果4 小时前
Flutter 路由进阶:命名路由、动态路由与路由守卫实现
开发语言·javascript·flutter·华为·ecmascript
世转神风-4 小时前
qt-lambda信号槽机制
开发语言·qt