第18章 网络编程:TCP/UDP通信、QNetworkAccessManager、HTTP请求、WebSocket、网络下载管理器
网络编程,说白了就是让程序跟外界说话。我刚开始学Qt网络模块时,总觉得这东西很神秘——数据怎么就从一台机器跑到另一台机器上了?后来做项目多了,发现其实没那么玄乎。Qt把底层网络细节都封装好了,我们只需要调用几个类,就能实现各种网络通信。
这一章我会带你走一遍Qt网络编程的核心内容。从最基础的TCP/UDP,到HTTP请求,再到WebSocket实时通信,最后做一个实用的网络下载管理器。嗯,这些都是我在实际项目中反复用到的技术。
18.1 TCP通信:可靠的连接
TCP就像打电话。你先拨号,对方接听,然后你们开始对话。整个过程是可靠的,数据不会丢,顺序也不会乱。
Qt里实现TCP通信,主要用两个类:QTcpServer和QTcpSocket。前者负责监听端口、接受连接,后者负责收发数据。
服务端代码示例
// 服务端:监听连接
QTcpServer *server = new QTcpServer(this);
if (!server->listen(QHostAddress::Any, 8888)) {
qDebug() << "监听失败:" << server->errorString();
return;
}
connect(server, &QTcpServer::newConnection, this, [=]() {
QTcpSocket *socket = server->nextPendingConnection();
qDebug() << "新客户端连接:" << socket->peerAddress().toString();
// 读取数据
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QByteArray data = socket->readAll();
qDebug() << "收到数据:" << data;
socket->write("已收到: " + data);
});
});
客户端代码示例
// 客户端:连接服务端
QTcpSocket *socket = new QTcpSocket(this);
socket->connectToHost("127.0.0.1", 8888);
connect(socket, &QTcpSocket::connected, this, [=]() {
qDebug() << "连接成功";
socket->write("Hello Server!");
});
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QByteArray response = socket->readAll();
qDebug() << "服务端回复:" << response;
});
我的经验:TCP通信一定要处理好粘包问题。我早期做项目时,客户端一次性发了两条消息,服务端一次readAll全读出来了,结果解析出错。后来我习惯在每条消息前加4字节的长度头,这样接收方先读长度,再读指定字节的数据,就不会乱了。
18.2 UDP通信:快但不可靠
UDP就像发短信。你发出去就完了,对方收没收到你不知道,顺序也可能乱。但好处是快,没有连接建立的开销。
Qt里用QUdpSocket实现UDP通信。不需要建立连接,直接发数据就行。
// UDP发送端
QUdpSocket *udpSocket = new QUdpSocket(this);
QByteArray data = "Hello UDP!";
udpSocket->writeDatagram(data, QHostAddress::Broadcast, 9999);
// UDP接收端
QUdpSocket *udpReceiver = new QUdpSocket(this);
udpReceiver->bind(9999);
connect(udpReceiver, &QUdpSocket::readyRead, this, [=]() {
while (udpReceiver->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(udpReceiver->pendingDatagramSize());
udpReceiver->readDatagram(datagram.data(), datagram.size());
qDebug() << "收到UDP数据:" << datagram;
}
});
注意:UDP数据报有大小限制,一般不超过65507字节。如果你要发大文件,别用UDP,老老实实用TCP或者自己分包。我曾经试过用UDP发一张图片,结果数据被截断了,图片显示不出来——嗯,那是个教训。
18.3 QNetworkAccessManager:HTTP请求的瑞士军刀
做网络应用,HTTP请求是家常便饭。Qt提供了QNetworkAccessManager,这个类封装了GET、POST、PUT、DELETE等所有常见HTTP操作。
我个人习惯把QNetworkAccessManager设计成单例,整个程序只用一个实例。这样连接复用效率更高。
GET请求示例
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl("https://api.example.com/data"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, [=]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
qDebug() << "请求成功:" << responseData;
} else {
qDebug() << "请求失败:" << reply->errorString();
}
reply->deleteLater();
});
POST请求示例
// 发送JSON数据
QJsonObject json;
json["name"] = "Qt";
json["version"] = 6.5;
QNetworkRequest request(QUrl("https://api.example.com/submit"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = manager->post(request, QJsonDocument(json).toJson());
connect(reply, &QNetworkReply::finished, this, [=]() {
// 处理响应...
});
核心要点:记得在请求完成后调用reply->deleteLater()释放资源。不然内存泄漏会让你头疼。我见过一个同事的程序跑了三天后崩溃,查了半天发现是网络请求的reply对象没释放,堆了上万个。
18.4 WebSocket:实时双向通信
WebSocket让服务器可以主动给客户端发消息。这在聊天、实时数据推送场景下特别有用。
Qt的WebSocket模块用起来很直观。客户端用QWebSocket,服务端用QWebSocketServer。
// WebSocket客户端
QWebSocket *webSocket = new QWebSocket();
webSocket->open(QUrl("ws://localhost:12345"));
connect(webSocket, &QWebSocket::connected, this, [=]() {
qDebug() << "WebSocket连接成功";
webSocket->sendTextMessage("Hello Server!");
});
connect(webSocket, &QWebSocket::textMessageReceived,
this, [=](const QString &message) {
qDebug() << "收到服务端消息:" << message;
});
// WebSocket服务端
QWebSocketServer *server = new QWebSocketServer("MyServer",
QWebSocketServer::NonSecureMode, this);
if (server->listen(QHostAddress::Any, 12345)) {
connect(server, &QWebSocketServer::newConnection,
this, [=]() {
QWebSocket *socket = server->nextPendingConnection();
connect(socket, &QWebSocket::textMessageReceived,
this, [=](const QString &message) {
qDebug() << "收到:" << message;
socket->sendTextMessage("已收到: " + message);
});
});
}
避坑指南:我曾经在WebSocket的onClose事件里忘记清理socket对象,导致内存泄漏。建议在disconnected信号里调用socket->deleteLater()。另外,生产环境一定要用wss://加密连接,不然数据明文传输,太危险了。
18.5 网络下载管理器
最后,我们来做一个实用的下载管理器。它支持断点续传、进度显示、并发下载。
核心思路是用QNetworkAccessManager发起GET请求,设置Range头实现断点续传,用downloadProgress信号更新进度。
class DownloadManager : public QObject {
Q_OBJECT
public:
void startDownload(const QUrl &url, const QString &savePath) {
QNetworkRequest request(url);
// 检查本地文件,实现断点续传
QFile *file = new QFile(savePath);
if (file->exists()) {
qint64 existingSize = file->size();
request.setRawHeader("Range",
QString("bytes=%1-").arg(existingSize).toUtf8());
}
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::readyRead, this, [=]() {
file->write(reply->readAll());
});
connect(reply, &QNetworkReply::downloadProgress,
this, [=](qint64 received, qint64 total) {
int percent = (total > 0) ? (received * 100 / total) : 0;
emit progressChanged(url.toString(), percent);
});
connect(reply, &QNetworkReply::finished, this, [=]() {
file->close();
reply->deleteLater();
emit downloadFinished(url.toString());
});
}
signals:
void progressChanged(const QString &url, int percent);
void downloadFinished(const QString &url);
private:
QNetworkAccessManager *manager;
};
实用技巧:并发下载时,建议限制最大连接数。我一般设3-5个并发,太多会占用带宽,反而拖慢整体速度。另外,下载过程中要定期保存已下载的字节数,这样程序崩溃后还能续传。
知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系。你看一眼就能明白Qt网络编程的全貌。
你看,Qt网络编程其实就这几个核心模块。TCP和UDP是底层通信,HTTP和WebSocket是应用层协议,下载管理器则是综合应用。掌握了这些,大部分网络需求都能搞定。
实际开发中,我建议你从HTTP请求入手,因为用得最多。等需要实时通信了,再研究WebSocket。TCP和UDP嘛,做底层工具或游戏时才会用到。嗯,循序渐进就好。