2. QTcpSocket基础:从连接到断开的完整旅程
说实话,QTcpSocket 是我在 Qt 网络编程里打交道最多的类。没有之一。
你想想看,不管是写一个聊天软件,还是做一个数据采集系统,底层都离不开它。今天我就带你把它彻底搞明白。
2.1 QTcpSocket 是什么?
QTcpSocket 是 Qt 提供的 TCP 客户端套接字类。它继承自 QAbstractSocket,而 QAbstractSocket 又继承自 QIODevice。
这意味着什么?
意味着它不仅能读写网络数据,还能像操作文件一样用 read()、write() 这些接口。我当年刚接触时,就觉得这个设计特别巧妙——网络通信和文件操作统一了。
核心继承链:
QObject → QIODevice → QAbstractSocket → QTcpSocket
QTcpSocket 是异步的。它不会阻塞你的 UI 线程。这一点非常重要。
我记得有个新手同事,直接在 UI 线程里用 waitForConnected(),结果界面卡死了。嗯,这个坑我后面会细说。
2.2 连接服务器
连接服务器,说白了就是调用 connectToHost()。
QTcpSocket *socket = new QTcpSocket(this);
socket->connectToHost("192.168.1.100", 8080);
就这么简单?对,就这么简单。但背后的事情可不少。
connectToHost() 是异步的。它发起连接请求后立即返回。真正的连接结果,通过信号告诉你。
你需要监听两个关键信号:
- connected() — 连接成功
- errorOccurred(QAbstractSocket::SocketError) — 连接失败
我的习惯:连接前先检查 socket 状态。如果已经连接,先断开再重连。避免重复连接导致奇怪的问题。
if (socket->state() != QAbstractSocket::UnconnectedState) {
socket->abort(); // 立即断开,不等待
}
socket->connectToHost("192.168.1.100", 8080);
为什么会用 abort() 而不是 disconnectFromHost()?
abort() 会立即关闭套接字,不等待数据发送完毕。而 disconnectFromHost() 会优雅地关闭。在重连场景下,我倾向于用 abort(),因为快。
2.3 发送数据
发送数据用 write()。它继承自 QIODevice。
QByteArray data = "Hello, Server!";
qint64 bytesWritten = socket->write(data);
write() 返回实际写入的字节数。但注意,这个写入是指写入到 Qt 内部的发送缓冲区,不是直接发送到网络。
真正的数据发送,由 Qt 的事件循环在后台完成。数据发送完毕后,会触发 bytesWritten(qint64) 信号。
我曾经踩过的坑:连续多次调用 write() 发送小数据包。你以为发了 10 次,实际上底层可能合并成一个包发送了。这叫 Nagle 算法。如果你需要实时性,可以调用 setSocketOption(QAbstractSocket::LowDelayOption, 1) 禁用它。
我个人习惯发送数据时,先判断 socket 状态:
if (socket->state() == QAbstractSocket::ConnectedState) {
socket->write(data);
} else {
qWarning() << "Socket not connected!";
}
嗯,这个检查虽然简单,但能避免很多莫名其妙的崩溃。
2.4 接收数据
接收数据是 QTcpSocket 里最容易出问题的地方。
核心信号是 readyRead()。当有新数据到达时,Qt 会发射这个信号。
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QByteArray data = socket->readAll();
qDebug() << "Received:" << data;
});
看起来很简单对吧?但实际项目中,你可能会遇到这些问题:
- 粘包问题:一次 readyRead() 可能收到多个数据包
- 半包问题:一个完整的数据包可能分多次 readyRead() 到达
怎么解决?
我常用的方法是:自定义协议头。比如前 4 个字节表示数据长度。
void MyClass::onReadyRead() {
// 先检查是否收到足够的数据
if (socket->bytesAvailable() < 4) return;
// 读取长度
QByteArray lenData = socket->read(4);
quint32 totalLen = *reinterpret_cast<quint32*>(lenData.data());
// 等待完整数据
if (socket->bytesAvailable() < totalLen) return;
// 读取完整数据
QByteArray payload = socket->read(totalLen);
processPayload(payload);
}
避坑指南:不要在 readyRead() 里做耗时操作。我曾经在回调里写数据库,结果 UI 卡顿严重。正确的做法是:收到数据后,用信号抛出去,或者丢到工作线程处理。
2.5 断开连接
断开连接有两种方式:
| 方法 | 行为 | 适用场景 |
|---|---|---|
| disconnectFromHost() | 优雅关闭,等待数据发送完毕 | 正常退出 |
| abort() | 立即关闭,丢弃未发送数据 | 异常处理、快速重连 |
断开连接后,会触发 disconnected() 信号。
connect(socket, &QTcpSocket::disconnected, this, [=]() {
qDebug() << "Disconnected from server";
// 可以在这里做清理工作
});
我个人习惯在析构函数里主动断开:
MyClass::~MyClass() {
if (socket) {
socket->disconnectFromHost();
if (socket->state() != QAbstractSocket::UnconnectedState) {
socket->waitForDisconnected(1000); // 最多等1秒
}
}
}
为什么要等?因为 disconnectFromHost() 是异步的。如果不等待,直接销毁 socket,可能导致数据丢失。
2.6 核心流程图
下面这张图,是我用 SVG 画的 QTcpSocket 生命周期。你看一眼就能明白整个流程。
2.7 完整示例代码
最后,给你一个完整的客户端示例。麻雀虽小,五脏俱全。
class TcpClient : public QObject {
Q_OBJECT
public:
TcpClient(QObject *parent = nullptr) : QObject(parent) {
socket = new QTcpSocket(this);
connect(socket, &QTcpSocket::connected, this, [=]() {
qDebug() << "Connected!";
});
connect(socket, &QTcpSocket::disconnected, this, [=]() {
qDebug() << "Disconnected!";
});
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QByteArray data = socket->readAll();
qDebug() << "Received:" << data;
});
connect(socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError err) {
qDebug() << "Error:" << socket->errorString();
});
}
void connectToServer(const QString &host, quint16 port) {
if (socket->state() != QAbstractSocket::UnconnectedState) {
socket->abort();
}
socket->connectToHost(host, port);
}
void sendData(const QByteArray &data) {
if (socket->state() == QAbstractSocket::ConnectedState) {
socket->write(data);
}
}
void disconnect() {
socket->disconnectFromHost();
}
private:
QTcpSocket *socket;
};
好了,QTcpSocket 的基础就这些。你把它跑一遍,再结合我提到的那些坑,基本就能上手了。