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秒钟。用户还以为程序崩溃了。
正确的做法是用信号槽监听连接结果:
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%,程序直接无响应。
下面是一个带退避的重连示例:
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客户端说白了就是「连、发、收、断」四个动作,但每个动作背后都有不少细节。我建议你动手写一个简单的客户端,把连接、重连、数据收发都跑一遍。遇到问题不要慌,先检查socket状态,再检查数据缓存逻辑——八成问题都出在这两个地方。
可可精品资料库