3. TCP客户端实现:QTcpSocket的连接、断开与重连、数据收发基础

好,咱们今天来聊聊TCP客户端。说实话,TCP客户端是网络通信里最基础、也最常用的角色。你想想看,不管是聊天软件、游戏客户端,还是工业设备的上位机,底层几乎都是这么一套东西。

我个人习惯把TCP客户端比作「打电话」。你要先拨号(连接),通话中说话(发送数据),听对方说话(接收数据),最后挂断(断开)。嗯,就这么简单。但实际写代码时,坑可不少。

3.1 创建与连接:从new到connected

先看最基础的——怎么创建一个QTcpSocket对象并连上服务器。

// 创建socket
QTcpSocket *socket = new QTcpSocket(this);

// 连接到服务器
socket->connectToHost("192.168.1.100", 8080);

// 检查连接是否成功
if (socket->waitForConnected(3000)) {
    qDebug() << "连接成功!";
} else {
    qDebug() << "连接失败:" << socket->errorString();
}

这里有个细节我要强调一下:waitForConnected 是阻塞的。我在项目中遇到过,有人直接在UI线程里调这个,结果界面卡死3秒钟。用户还以为程序崩溃了。

警告: 不要在UI线程中使用阻塞式waitForConnected。建议用信号槽方式,或者把socket操作放到子线程里。

正确的做法是用信号槽监听连接结果:

connect(socket, &QTcpSocket::connected, this, [=]() {
    qDebug() << "连接成功!可以开始通信了。";
});

connect(socket, &QTcpSocket::disconnected, this, [=]() {
    qDebug() << "连接已断开。";
});

connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),
        this, [=](QAbstractSocket::SocketError error) {
    qDebug() << "发生错误:" << socket->errorString();
});

// 发起连接(非阻塞)
socket->connectToHost("192.168.1.100", 8080);

你看,这样写就不会卡界面了。连接成功或失败,系统会通过信号通知你。

3.2 断开与重连:优雅地结束,机智地恢复

断开连接有两种方式:

  • 主动断开:调用 disconnectFromHost()
  • 被动断开:服务器关闭、网络异常等

我建议主动断开时,先调用 disconnectFromHost(),再调用 waitForDisconnected() 等待确认。这样能保证数据发送完毕再断。

void disconnectGracefully(QTcpSocket *socket) {
    if (socket->state() == QAbstractSocket::ConnectedState) {
        socket->disconnectFromHost();
        if (socket->state() != QAbstractSocket::UnconnectedState) {
            socket->waitForDisconnected(3000);
        }
    }
}

重连这块,我曾经踩过一个大坑。当时做一个设备监控项目,网络偶尔闪断,我直接写了个死循环重连。结果呢?CPU飙到100%,程序直接无响应。

经验之谈: 重连一定要加延时和退避策略。比如第一次等1秒,第二次等2秒,第三次等4秒……最大不超过30秒。

下面是一个带退避的重连示例:

class TcpClient : public QObject {
    Q_OBJECT
public:
    TcpClient(QObject *parent = nullptr) : QObject(parent) {
        socket = new QTcpSocket(this);
        connect(socket, &QTcpSocket::disconnected, this, &TcpClient::onDisconnected);
        connect(socket, &QTcpSocket::connected, this, &TcpClient::onConnected);
    }

private slots:
    void onDisconnected() {
        qDebug() << "连接断开,准备重连...";
        reconnectTimer = new QTimer(this);
        reconnectTimer->setSingleShot(true);
        connect(reconnectTimer, &QTimer::timeout, this, [=]() {
            reconnectAttempts++;
            int delay = qMin(1000 * reconnectAttempts, 30000); // 最大30秒
            qDebug() << "第" << reconnectAttempts << "次重连,等待" << delay << "毫秒";
            socket->connectToHost("192.168.1.100", 8080);
        });
        reconnectTimer->start(1000); // 1秒后开始重连
    }

    void onConnected() {
        qDebug() << "重连成功!";
        reconnectAttempts = 0;
        if (reconnectTimer) {
            reconnectTimer->stop();
            delete reconnectTimer;
            reconnectTimer = nullptr;
        }
    }

private:
    QTcpSocket *socket;
    QTimer *reconnectTimer = nullptr;
    int reconnectAttempts = 0;
};

3.3 数据收发:write与readyRead的默契配合

发送数据很简单,调用 write() 就行:

// 发送文本
socket->write("Hello, Server!");

// 发送二进制数据
QByteArray data;
data.append(0x01);
data.append(0x02);
data.append(0x03);
socket->write(data);

但接收数据就没那么直接了。你想想看,TCP是流式协议,数据像水流一样过来。你调用一次 readAll(),可能只读到半个包,也可能一次读到好几个包。

这就是所谓的「粘包拆包」问题。我刚开始做网络编程时,就因为这个bug查了整整两天。

核心思路: 不要直接处理原始数据,先缓存起来,等凑够一个完整的数据包再处理。

下面是一个简单的缓存处理示例:

// 在类中定义
QByteArray buffer;

// 连接readyRead信号
connect(socket, &QTcpSocket::readyRead, this, [=]() {
    // 1. 先把新数据追加到缓存
    buffer.append(socket->readAll());

    // 2. 循环处理缓存中的完整包
    while (true) {
        // 假设我们的协议是:前4字节表示数据长度(大端序)
        if (buffer.size() < 4) {
            break; // 还不够包头,继续等
        }

        // 解析包体长度
        quint32 bodyLen = qFromBigEndian<quint32>(
            reinterpret_cast<const uchar*>(buffer.constData()));

        // 检查是否收齐了整个包
        if (buffer.size() < 4 + bodyLen) {
            break; // 包体还没收完,继续等
        }

        // 取出完整的数据包
        QByteArray packet = buffer.mid(4, bodyLen);
        buffer.remove(0, 4 + bodyLen);

        // 处理这个完整的数据包
        processPacket(packet);
    }
});

你看,这个逻辑其实不复杂。但为什么很多人写不对?说白了就是没理解「TCP是流」这个本质。你把它当成UDP那种「发一次收一次」的思维,肯定要出问题。

3.4 状态管理:别让socket状态失控

QTcpSocket有几种状态,我建议你时刻关注:

状态 含义 常见场景
UnconnectedState 未连接 初始状态、断开后
HostLookupState 正在解析域名 connectToHost时
ConnectingState 正在连接 TCP三次握手阶段
ConnectedState 已连接 可以收发数据
ClosingState 正在关闭 调用disconnectFromHost后

我习惯在状态变化时打印日志,方便调试:

connect(socket, &QTcpSocket::stateChanged, this, [=](QAbstractSocket::SocketState state) {
    qDebug() << "Socket状态变化:" << state;
});

嗯,这里要注意:不要在状态回调里直接操作socket。比如在disconnected信号里直接调用connectToHost,可能会触发递归。用QTimer::singleShot延迟一下更安全。

3.5 本章知识体系

下面这张图总结了TCP客户端核心流程,我建议你保存下来,写代码时对照着看:

TCP客户端核心流程 创建QTcpSocket connectToHost() connected信号触发 write() / readyRead信号 disconnectFromHost() 关键要点 • 非阻塞连接,用信号监听 • 缓存数据,解决粘包拆包 • 重连加退避,避免死循环 • 状态变化要打日志 • 不要在回调里直接操作socket • 主动断开要优雅等待 • 区分文本和二进制数据 • 错误处理要完善 • 考虑心跳保活机制 • 多线程下注意线程安全

好了,这一章的内容就这些。TCP客户端说白了就是「连、发、收、断」四个动作,但每个动作背后都有不少细节。我建议你动手写一个简单的客户端,把连接、重连、数据收发都跑一遍。遇到问题不要慌,先检查socket状态,再检查数据缓存逻辑——八成问题都出在这两个地方。

最后一个小技巧: 调试网络程序时,开一个Wireshark抓包,看看实际发送和接收的数据。很多时候代码看起来没问题,但网络层就是不对。抓包一看,哦,原来是字节序搞反了。

可可精品资料库