📋 第六章:网络编程最佳实践
本章总结Qt网络编程中的常见问题和最佳解决方案,帮助你写出健壮、高效的网络应用。
6.1 错误处理
6.1.1 网络错误分类
cpp
// QNetworkReply::NetworkError 常见错误码
void handleNetworkError(QNetworkReply::NetworkError error) {
switch (error) {
// 连接错误
case QNetworkReply::ConnectionRefusedError:
qDebug() << "❌ 连接被拒绝";
break;
case QNetworkReply::RemoteHostClosedError:
qDebug() << "❌ 远程主机关闭连接";
break;
case QNetworkReply::HostNotFoundError:
qDebug() << "❌ 找不到主机";
break;
case QNetworkReply::TimeoutError:
qDebug() << "❌ 连接超时";
break;
// SSL错误
case QNetworkReply::SslHandshakeFailedError:
qDebug() << "❌ SSL握手失败";
break;
// HTTP错误
case QNetworkReply::ContentNotFoundError:
qDebug() << "❌ 404 - 资源不存在";
break;
case QNetworkReply::AuthenticationRequiredError:
qDebug() << "❌ 需要认证";
break;
case QNetworkReply::ContentAccessDenied:
qDebug() << "❌ 403 - 访问被拒绝";
break;
// 其他
case QNetworkReply::UnknownNetworkError:
qDebug() << "❌ 未知网络错误";
break;
default:
qDebug() << "❌ 错误码:" << error;
}
}
6.1.2 统一错误处理类
cpp
class NetworkErrorHandler {
public:
struct ErrorInfo {
int httpStatus;
QString code;
QString message;
bool retryable;
};
static ErrorInfo analyze(QNetworkReply *reply) {
ErrorInfo info;
info.httpStatus = reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute).toInt();
QNetworkReply::NetworkError err = reply->error();
// 可重试的错误
info.retryable = (err == QNetworkReply::TimeoutError ||
err == QNetworkReply::TemporaryNetworkFailureError ||
info.httpStatus == 502 ||
info.httpStatus == 503 ||
info.httpStatus == 504);
// 错误分类
if (err == QNetworkReply::NoError) {
info.code = "SUCCESS";
info.message = "成功";
} else if (info.httpStatus >= 400 && info.httpStatus < 500) {
info.code = "CLIENT_ERROR";
info.message = getClientErrorMessage(info.httpStatus, reply);
} else if (info.httpStatus >= 500) {
info.code = "SERVER_ERROR";
info.message = "服务器错误,请稍后重试";
} else if (err == QNetworkReply::TimeoutError) {
info.code = "TIMEOUT";
info.message = "请求超时,请检查网络";
} else if (err == QNetworkReply::HostNotFoundError) {
info.code = "DNS_ERROR";
info.message = "无法连接服务器,请检查网络";
} else {
info.code = "NETWORK_ERROR";
info.message = reply->errorString();
}
return info;
}
private:
static QString getClientErrorMessage(int status, QNetworkReply *reply) {
// 尝试解析服务器返回的错误信息
QByteArray body = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(body);
if (doc.isObject() && doc.object().contains("message")) {
return doc.object()["message"].toString();
}
// 默认消息
switch (status) {
case 400: return "请求参数错误";
case 401: return "未登录或登录已过期";
case 403: return "没有访问权限";
case 404: return "请求的资源不存在";
case 429: return "请求太频繁,请稍后再试";
default: return QString("请求失败(%1)").arg(status);
}
}
};
6.2 超时设置
6.2.1 请求超时配置
cpp
// Qt 5.15+ 推荐方式
void configureTimeout(QNetworkRequest &request) {
// 设置传输超时(毫秒)
request.setTransferTimeout(30000); // 30秒
}
// 兼容旧版本的方式
class TimeoutNetworkManager : public QObject {
Q_OBJECT
public:
TimeoutNetworkManager(int timeoutMs = 30000, QObject *parent = nullptr)
: QObject(parent), timeout(timeoutMs) {
manager = new QNetworkAccessManager(this);
}
QNetworkReply* get(const QNetworkRequest &request) {
QNetworkReply *reply = manager->get(request);
setupTimeout(reply);
return reply;
}
QNetworkReply* post(const QNetworkRequest &request, const QByteArray &data) {
QNetworkReply *reply = manager->post(request, data);
setupTimeout(reply);
return reply;
}
private:
void setupTimeout(QNetworkReply *reply) {
QTimer *timer = new QTimer(reply);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, reply, [reply]() {
if (reply->isRunning()) {
reply->abort();
qDebug() << "⏰ 请求超时,已中止";
}
});
connect(reply, &QNetworkReply::finished, timer, &QTimer::stop);
timer->start(timeout);
}
QNetworkAccessManager *manager;
int timeout;
};
6.2.2 分阶段超时
cpp
class PhaseTimeoutRequest : public QObject {
Q_OBJECT
public:
// 连接超时、读取超时、总超时
void setTimeouts(int connectMs, int readMs, int totalMs) {
connectTimeout = connectMs;
readTimeout = readMs;
totalTimeout = totalMs;
}
void get(const QUrl &url) {
QNetworkRequest request(url);
reply = manager->get(request);
// 总超时
totalTimer = new QTimer(this);
totalTimer->setSingleShot(true);
connect(totalTimer, &QTimer::timeout, this, &PhaseTimeoutRequest::onTotalTimeout);
totalTimer->start(totalTimeout);
// 连接超时(等待首个数据)
connectTimer = new QTimer(this);
connectTimer->setSingleShot(true);
connect(connectTimer, &QTimer::timeout, this, &PhaseTimeoutRequest::onConnectTimeout);
connectTimer->start(connectTimeout);
// 读取超时(连接后)
readTimer = new QTimer(this);
readTimer->setSingleShot(true);
connect(reply, &QNetworkReply::readyRead, this, [this]() {
connectTimer->stop(); // 有数据了,停止连接超时
readTimer->start(readTimeout); // 重置读取超时
});
connect(reply, &QNetworkReply::finished, this, [this]() {
totalTimer->stop();
connectTimer->stop();
readTimer->stop();
emit finished(reply);
});
}
signals:
void finished(QNetworkReply *reply);
void timeout(const QString &phase);
private slots:
void onConnectTimeout() {
qDebug() << "⏰ 连接超时";
reply->abort();
emit timeout("connect");
}
void onTotalTimeout() {
qDebug() << "⏰ 总超时";
reply->abort();
emit timeout("total");
}
private:
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkReply *reply = nullptr;
QTimer *connectTimer = nullptr;
QTimer *readTimer = nullptr;
QTimer *totalTimer = nullptr;
int connectTimeout = 10000;
int readTimeout = 30000;
int totalTimeout = 60000;
};
6.3 重试机制
6.3.1 指数退避重试
cpp
class RetryableRequest : public QObject {
Q_OBJECT
public:
RetryableRequest(QObject *parent = nullptr) : QObject(parent) {
manager = new QNetworkAccessManager(this);
}
void setRetryPolicy(int maxRetries, int baseDelayMs) {
this->maxRetries = maxRetries;
this->baseDelay = baseDelayMs;
}
void get(const QUrl &url) {
currentUrl = url;
retryCount = 0;
sendRequest();
}
signals:
void success(const QByteArray &data);
void failure(const QString &error);
void retrying(int attempt, int delayMs);
private:
void sendRequest() {
QNetworkRequest request(currentUrl);
request.setTransferTimeout(15000);
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
handleResponse(reply);
});
}
void handleResponse(QNetworkReply *reply) {
// 检查是否可重试
auto errorInfo = NetworkErrorHandler::analyze(reply);
if (reply->error() == QNetworkReply::NoError) {
emit success(reply->readAll());
} else if (errorInfo.retryable && retryCount < maxRetries) {
// 计算延迟:指数退避 + 随机抖动
int delay = baseDelay * qPow(2, retryCount);
delay += QRandomGenerator::global()->bounded(1000); // 0-1秒抖动
retryCount++;
qDebug() << QString("🔄 第%1次重试,%2ms后执行")
.arg(retryCount).arg(delay);
emit retrying(retryCount, delay);
QTimer::singleShot(delay, this, &RetryableRequest::sendRequest);
} else {
emit failure(errorInfo.message);
}
reply->deleteLater();
}
QNetworkAccessManager *manager;
QUrl currentUrl;
int retryCount = 0;
int maxRetries = 3;
int baseDelay = 1000; // 1秒
};
// 使用示例
void testRetry() {
RetryableRequest *req = new RetryableRequest();
req->setRetryPolicy(3, 1000); // 最多3次,初始延迟1秒
QObject::connect(req, &RetryableRequest::retrying,
[](int attempt, int delay) {
qDebug() << "第" << attempt << "次重试," << delay << "ms后";
});
QObject::connect(req, &RetryableRequest::success,
[](const QByteArray &data) {
qDebug() << "成功:" << data.left(100);
});
req->get(QUrl("https://api.example.com/data"));
}
6.4 性能优化
6.4.1 连接复用
cpp
// ✅ 正确:使用单例管理器
class HttpManager {
public:
static QNetworkAccessManager* instance() {
static QNetworkAccessManager manager;
return &manager;
}
};
// 使用
QNetworkReply *reply = HttpManager::instance()->get(request);
// ❌ 错误:每次请求创建新管理器(会创建新连接)
void badExample() {
QNetworkAccessManager *manager = new QNetworkAccessManager();
manager->get(request); // 无法复用连接
}
6.4.2 请求合并
cpp
class BatchRequest : public QObject {
Q_OBJECT
public:
BatchRequest(int batchSize = 10, int intervalMs = 100, QObject *parent = nullptr)
: QObject(parent), batchSize(batchSize) {
manager = new QNetworkAccessManager(this);
batchTimer = new QTimer(this);
connect(batchTimer, &QTimer::timeout, this, &BatchRequest::flush);
batchTimer->start(intervalMs);
}
void enqueue(const QString &id, const QUrl &url) {
pendingRequests.append({id, url});
if (pendingRequests.size() >= batchSize) {
flush();
}
}
signals:
void completed(const QString &id, const QByteArray &data);
private slots:
void flush() {
if (pendingRequests.isEmpty()) return;
// 批量发送
auto batch = pendingRequests.mid(0, batchSize);
pendingRequests = pendingRequests.mid(batchSize);
for (const auto &req : batch) {
QNetworkReply *reply = manager->get(QNetworkRequest(req.url));
QString id = req.id;
connect(reply, &QNetworkReply::finished, this, [this, reply, id]() {
emit completed(id, reply->readAll());
reply->deleteLater();
});
}
}
private:
struct PendingRequest {
QString id;
QUrl url;
};
QNetworkAccessManager *manager;
QList<PendingRequest> pendingRequests;
QTimer *batchTimer;
int batchSize;
};
6.4.3 响应缓存
cpp
class CachedNetworkManager : public QObject {
Q_OBJECT
public:
CachedNetworkManager(QObject *parent = nullptr) : QObject(parent) {
manager = new QNetworkAccessManager(this);
// 设置磁盘缓存
cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(QStandardPaths::writableLocation(
QStandardPaths::CacheLocation) + "/http_cache");
cache->setMaximumCacheSize(50 * 1024 * 1024); // 50MB
manager->setCache(cache);
}
void get(const QUrl &url, bool forceRefresh = false) {
QNetworkRequest request(url);
if (forceRefresh) {
// 强制刷新,不使用缓存
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
QNetworkRequest::AlwaysNetwork);
} else {
// 优先使用缓存
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
QNetworkRequest::PreferCache);
}
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
bool fromCache = reply->attribute(
QNetworkRequest::SourceIsFromCacheAttribute).toBool();
qDebug() << "数据来源:" << (fromCache ? "缓存" : "网络");
emit dataReceived(reply->readAll(), fromCache);
reply->deleteLater();
});
}
signals:
void dataReceived(const QByteArray &data, bool fromCache);
private:
QNetworkAccessManager *manager;
QNetworkDiskCache *cache;
};
6.5 安全性
6.5.1 敏感数据保护
cpp
// ❌ 错误:明文传输敏感数据
void badLogin(const QString &username, const QString &password) {
QUrl url("http://api.example.com/login"); // HTTP!
// 密码明文传输...
}
// ✅ 正确:使用HTTPS
void secureLogin(const QString &username, const QString &password) {
QNetworkRequest request(QUrl("https://api.example.com/login"));
// 设置SSL配置
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
sslConfig.setProtocol(QSsl::TlsV1_2OrLater);
request.setSslConfiguration(sslConfig);
// 使用POST请求,密码不出现在URL中
QJsonObject body;
body["username"] = username;
body["password"] = password; // 通过HTTPS加密传输
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
manager->post(request, QJsonDocument(body).toJson());
}
6.5.2 Token安全存储
cpp
#include <QKeychain/Keychain> // 使用QtKeychain库
class SecureTokenStorage {
public:
// 安全存储Token
static void saveToken(const QString &token) {
QKeychain::WritePasswordJob job("MyApp");
job.setKey("auth_token");
job.setTextData(token);
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
qDebug() << "保存Token失败:" << job.errorString();
}
}
// 读取Token
static QString loadToken() {
QKeychain::ReadPasswordJob job("MyApp");
job.setKey("auth_token");
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
return QString();
}
return job.textData();
}
// 删除Token
static void deleteToken() {
QKeychain::DeletePasswordJob job("MyApp");
job.setKey("auth_token");
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
}
};
6.5.3 防止常见攻击
cpp
class SecurityHelper {
public:
// URL参数编码,防止注入
static QString encodeParam(const QString &value) {
return QUrl::toPercentEncoding(value);
}
// 验证URL,防止SSRF
static bool isValidUrl(const QUrl &url) {
// 只允许https
if (url.scheme() != "https") {
return false;
}
// 黑名单检查(禁止内网地址)
QString host = url.host();
if (host == "localhost" ||
host.startsWith("127.") ||
host.startsWith("192.168.") ||
host.startsWith("10.") ||
host.startsWith("172.16.")) {
return false;
}
// 白名单检查
static QStringList allowedDomains = {
"api.example.com",
"cdn.example.com"
};
for (const QString &domain : allowedDomains) {
if (host == domain || host.endsWith("." + domain)) {
return true;
}
}
return false;
}
// 生成请求签名
static QString signRequest(const QString &method,
const QString &path,
const QString ×tamp,
const QString &secretKey) {
QString payload = method + "\n" + path + "\n" + timestamp;
QByteArray signature = QMessageAuthenticationCode::hash(
payload.toUtf8(),
secretKey.toUtf8(),
QCryptographicHash::Sha256
);
return signature.toBase64();
}
};
6.6 调试与日志
6.6.1 网络请求日志
cpp
class NetworkLogger : public QObject {
Q_OBJECT
public:
static void install(QNetworkAccessManager *manager) {
connect(manager, &QNetworkAccessManager::finished,
[](QNetworkReply *reply) {
logResponse(reply);
});
}
private:
static void logResponse(QNetworkReply *reply) {
QString method = reply->request().attribute(
QNetworkRequest::CustomVerbAttribute).toString();
if (method.isEmpty()) method = "GET";
int status = reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString url = reply->url().toString();
qint64 elapsed = reply->request().attribute(
QNetworkRequest::User).toLongLong(); // 如果设置了
QString log = QString("[HTTP] %1 %2 -> %3")
.arg(method)
.arg(url)
.arg(status);
if (reply->error() != QNetworkReply::NoError) {
qWarning() << log << "ERROR:" << reply->errorString();
} else {
qDebug() << log;
}
}
};
// 使用
NetworkLogger::install(HttpManager::instance());
第六章小结
本章总结了Qt网络编程的最佳实践:
| 主题 | 要点 |
|---|---|
| 错误处理 | 分类处理、友好提示、错误分析类 |
| 超时设置 | 连接/读取/总超时分阶段 |
| 重试机制 | 指数退避、最大次数、可重试判断 |
| 性能优化 | 连接复用、请求合并、响应缓存 |
| 安全性 | HTTPS、Token安全存储、防注入 |
| 调试日志 | 请求/响应日志记录 |
核心原则:
cpp
// 1. 始终使用异步请求
connect(reply, &QNetworkReply::finished, ...); // ✅
// 2. 始终处理错误
if (reply->error() != QNetworkReply::NoError) { ... }
// 3. 始终释放Reply
reply->deleteLater();
// 4. 使用单例管理器
HttpManager::instance()->get(request);
// 5. 设置合理超时
request.setTransferTimeout(30000);
// 6. 实现重试逻辑
if (isRetryable(error)) { retry(); }
📋 第七章:实战项目
本章提供多个完整可执行的网络编程实战项目,每个项目都包含完整的源代码和项目配置。
7.1 项目一:简易TCP聊天程序
7.1.1 项目概述
一个基于TCP的简易聊天程序,包含服务端和客户端,支持多客户端同时连接和消息广播。
功能特点:
- 服务端支持多客户端连接
- 消息广播给所有在线用户
- 用户上线/下线通知
- 简洁的命令行界面
7.1.2 项目结构
ChatDemo/
├── ChatServer/
│ ├── ChatServer.pro
│ ├── main.cpp
│ ├── chatserver.h
│ └── chatserver.cpp
└── ChatClient/
├── ChatClient.pro
├── main.cpp
├── chatclient.h
└── chatclient.cpp
7.1.3 服务端完整代码
qmake
QT += core network
QT -= gui
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = ChatServer
TEMPLATE = app
SOURCES += main.cpp chatserver.cpp
HEADERS += chatserver.h
chatserver.h:
cpp
#ifndef CHATSERVER_H
#define CHATSERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMap>
#include <QDateTime>
class ChatServer : public QObject
{
Q_OBJECT
public:
explicit ChatServer(quint16 port, QObject *parent = nullptr);
bool start();
void stop();
private slots:
void onNewConnection();
void onClientDisconnected();
void onReadyRead();
private:
void broadcast(const QString &message, QTcpSocket *excludeClient = nullptr);
void sendToClient(QTcpSocket *client, const QString &message);
QString formatMessage(const QString &sender, const QString &content);
QTcpServer *m_server;
QMap<QTcpSocket*, QString> m_clients; // socket -> 用户名
quint16 m_port;
};
#endif // CHATSERVER_H
chatserver.cpp:
cpp
#include "chatserver.h"
#include <QDebug>
#include <QDataStream>
ChatServer::ChatServer(quint16 port, QObject *parent)
: QObject(parent), m_port(port)
{
m_server = new QTcpServer(this);
connect(m_server, &QTcpServer::newConnection,
this, &ChatServer::onNewConnection);
}
bool ChatServer::start()
{
if (m_server->listen(QHostAddress::Any, m_port)) {
qDebug() << "========================================";
qDebug() << " 聊天服务器已启动";
qDebug() << " 监听端口:" << m_port;
qDebug() << "========================================";
return true;
}
qDebug() << "服务器启动失败:" << m_server->errorString();
return false;
}
void ChatServer::stop()
{
// 关闭所有客户端连接
for (QTcpSocket *client : m_clients.keys()) {
client->disconnectFromHost();
}
m_clients.clear();
m_server->close();
qDebug() << "服务器已停止";
}
void ChatServer::onNewConnection()
{
QTcpSocket *client = m_server->nextPendingConnection();
connect(client, &QTcpSocket::disconnected,
this, &ChatServer::onClientDisconnected);
connect(client, &QTcpSocket::readyRead,
this, &ChatServer::onReadyRead);
// 暂时用地址作为临时标识
QString tempName = QString("user_%1").arg(client->peerPort());
m_clients[client] = tempName;
qDebug() << "[连接]" << client->peerAddress().toString()
<< ":" << client->peerPort();
// 发送欢迎消息
sendToClient(client, "[系统] 欢迎加入聊天室!请发送 /name 你的昵称 设置昵称");
// 通知其他人
broadcast(QString("[系统] %1 加入了聊天室").arg(tempName), client);
}
void ChatServer::onClientDisconnected()
{
QTcpSocket *client = qobject_cast<QTcpSocket*>(sender());
if (!client) return;
QString username = m_clients.take(client);
qDebug() << "[断开]" << username;
// 通知其他人
broadcast(QString("[系统] %1 离开了聊天室").arg(username));
client->deleteLater();
}
void ChatServer::onReadyRead()
{
QTcpSocket *client = qobject_cast<QTcpSocket*>(sender());
if (!client) return;
// 读取所有可用数据
QByteArray data = client->readAll();
QString message = QString::fromUtf8(data).trimmed();
if (message.isEmpty()) return;
QString username = m_clients.value(client);
// 处理命令
if (message.startsWith("/name ")) {
QString newName = message.mid(6).trimmed();
if (!newName.isEmpty() && newName.length() <= 20) {
QString oldName = username;
m_clients[client] = newName;
broadcast(QString("[系统] %1 更名为 %2").arg(oldName, newName));
qDebug() << "[更名]" << oldName << "->" << newName;
} else {
sendToClient(client, "[系统] 昵称无效(1-20个字符)");
}
}
else if (message == "/list") {
// 显示在线用户列表
QStringList users;
for (const QString &name : m_clients.values()) {
users << name;
}
sendToClient(client, QString("[系统] 在线用户(%1): %2")
.arg(users.size()).arg(users.join(", ")));
}
else if (message == "/help") {
sendToClient(client, "[帮助] 可用命令:\n"
" /name <昵称> - 设置昵称\n"
" /list - 查看在线用户\n"
" /quit - 退出聊天室\n"
" 直接输入文字发送消息");
}
else if (message == "/quit") {
client->disconnectFromHost();
}
else {
// 普通聊天消息,广播给所有人
QString formattedMsg = formatMessage(username, message);
broadcast(formattedMsg);
qDebug() << "[消息]" << username << ":" << message;
}
}
void ChatServer::broadcast(const QString &message, QTcpSocket *excludeClient)
{
QByteArray data = (message + "\n").toUtf8();
for (QTcpSocket *client : m_clients.keys()) {
if (client != excludeClient && client->state() == QAbstractSocket::ConnectedState) {
client->write(data);
client->flush();
}
}
}
void ChatServer::sendToClient(QTcpSocket *client, const QString &message)
{
if (client && client->state() == QAbstractSocket::ConnectedState) {
client->write((message + "\n").toUtf8());
client->flush();
}
}
QString ChatServer::formatMessage(const QString &sender, const QString &content)
{
QString time = QDateTime::currentDateTime().toString("hh:mm:ss");
return QString("[%1] %2: %3").arg(time, sender, content);
}
main.cpp(服务端):
cpp
#include <QCoreApplication>
#include "chatserver.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
quint16 port = 9999;
if (argc > 1) {
port = QString(argv[1]).toUShort();
}
ChatServer server(port);
if (!server.start()) {
return 1;
}
return a.exec();
}
7.1.4 客户端完整代码
qmake
QT += core network
QT -= gui
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = ChatClient
TEMPLATE = app
SOURCES += main.cpp chatclient.cpp
HEADERS += chatclient.h
chatclient.h:
cpp
#ifndef CHATCLIENT_H
#define CHATCLIENT_H
#include <QObject>
#include <QTcpSocket>
#include <QTextStream>
#include <QSocketNotifier>
class ChatClient : public QObject
{
Q_OBJECT
public:
explicit ChatClient(const QString &host, quint16 port, QObject *parent = nullptr);
void connectToServer();
private slots:
void onConnected();
void onDisconnected();
void onReadyRead();
void onError(QAbstractSocket::SocketError error);
void onStdinReady();
private:
void sendMessage(const QString &message);
QTcpSocket *m_socket;
QString m_host;
quint16 m_port;
QSocketNotifier *m_stdinNotifier;
QTextStream m_stdin;
};
#endif // CHATCLIENT_H
chatclient.cpp:
cpp
#include "chatclient.h"
#include <QDebug>
#include <QCoreApplication>
#ifdef Q_OS_WIN
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#endif
ChatClient::ChatClient(const QString &host, quint16 port, QObject *parent)
: QObject(parent), m_host(host), m_port(port), m_stdin(stdin)
{
m_socket = new QTcpSocket(this);
connect(m_socket, &QTcpSocket::connected,
this, &ChatClient::onConnected);
connect(m_socket, &QTcpSocket::disconnected,
this, &ChatClient::onDisconnected);
connect(m_socket, &QTcpSocket::readyRead,
this, &ChatClient::onReadyRead);
connect(m_socket, &QTcpSocket::errorOccurred,
this, &ChatClient::onError);
// 设置标准输入监听
#ifdef Q_OS_WIN
// Windows下需要特殊处理
_setmode(_fileno(stdin), _O_TEXT);
#endif
m_stdinNotifier = new QSocketNotifier(fileno(stdin),
QSocketNotifier::Read, this);
connect(m_stdinNotifier, &QSocketNotifier::activated,
this, &ChatClient::onStdinReady);
}
void ChatClient::connectToServer()
{
qDebug() << "正在连接服务器" << m_host << ":" << m_port << "...";
m_socket->connectToHost(m_host, m_port);
}
void ChatClient::onConnected()
{
qDebug() << "========================================";
qDebug() << " 已连接到聊天服务器";
qDebug() << " 输入 /help 查看帮助";
qDebug() << "========================================";
}
void ChatClient::onDisconnected()
{
qDebug() << "\n已断开连接";
QCoreApplication::quit();
}
void ChatClient::onReadyRead()
{
while (m_socket->canReadLine()) {
QByteArray line = m_socket->readLine();
QString message = QString::fromUtf8(line).trimmed();
if (!message.isEmpty()) {
qDebug().noquote() << message;
}
}
}
void ChatClient::onError(QAbstractSocket::SocketError error)
{
Q_UNUSED(error)
qDebug() << "连接错误:" << m_socket->errorString();
QCoreApplication::quit();
}
void ChatClient::onStdinReady()
{
QString line = m_stdin.readLine();
if (!line.isEmpty()) {
if (line == "/quit") {
m_socket->disconnectFromHost();
} else {
sendMessage(line);
}
}
}
void ChatClient::sendMessage(const QString &message)
{
if (m_socket->state() == QAbstractSocket::ConnectedState) {
m_socket->write((message + "\n").toUtf8());
m_socket->flush();
}
}
main.cpp(客户端):
cpp
#include <QCoreApplication>
#include "chatclient.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QString host = "127.0.0.1";
quint16 port = 9999;
if (argc > 1) host = argv[1];
if (argc > 2) port = QString(argv[2]).toUShort();
ChatClient client(host, port);
client.connectToServer();
return a.exec();
}
7.1.5 编译和运行
bash
# 编译服务端
cd ChatServer
qmake && make # 或 nmake (Windows)
# 编译客户端
cd ../ChatClient
qmake && make
# 运行
./ChatServer 9999 # 启动服务端
./ChatClient 127.0.0.1 9999 # 启动客户端
运行效果:
# 服务端输出
========================================
聊天服务器已启动
监听端口: 9999
========================================
[连接] 127.0.0.1 : 54321
[更名] user_54321 -> 张三
[消息] 张三 : 大家好!
# 客户端输出
========================================
已连接到聊天服务器
输入 /help 查看帮助
========================================
[系统] 欢迎加入聊天室!请发送 /name 你的昵称 设置昵称
/name 张三
[系统] user_54321 更名为 张三
大家好!
[14:30:25] 张三: 大家好!
7.2 项目二:HTTP文件下载器
7.2.1 项目概述
一个支持断点续传的HTTP文件下载器,提供命令行界面,显示下载进度和速度。
功能特点:
- 支持HTTP/HTTPS下载
- 断点续传(暂停后可继续)
- 实时显示下载进度和速度
- 命令行交互控制
7.2.2 项目结构
FileDownloader/
├── FileDownloader.pro
├── main.cpp
├── downloader.h
└── downloader.cpp
7.2.3 完整代码
qmake
QT += core network
QT -= gui
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = FileDownloader
TEMPLATE = app
SOURCES += main.cpp downloader.cpp
HEADERS += downloader.h
downloader.h:
cpp
#ifndef DOWNLOADER_H
#define DOWNLOADER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include <QTimer>
#include <QElapsedTimer>
#include <QUrl>
class Downloader : public QObject
{
Q_OBJECT
public:
explicit Downloader(QObject *parent = nullptr);
~Downloader();
void download(const QString &url, const QString &savePath);
void pause();
void resume();
void cancel();
bool isDownloading() const { return m_downloading; }
bool isPaused() const { return m_paused; }
signals:
void progressUpdated(qint64 bytesReceived, qint64 bytesTotal,
double speed, int remainingSeconds);
void finished(const QString &filePath);
void error(const QString &errorString);
void paused();
void resumed();
private slots:
void onReadyRead();
void onFinished();
void onError(QNetworkReply::NetworkError code);
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void updateSpeed();
private:
void startDownload(qint64 startByte = 0);
QString formatSize(qint64 bytes);
QString formatTime(int seconds);
QNetworkAccessManager *m_manager;
QNetworkReply *m_reply;
QFile *m_file;
QString m_url;
QString m_savePath;
QString m_tempPath;
qint64 m_downloadedBytes;
qint64 m_totalBytes;
qint64 m_lastBytes;
QElapsedTimer m_speedTimer;
QTimer *m_speedUpdateTimer;
bool m_downloading;
bool m_paused;
};
#endif // DOWNLOADER_H
downloader.cpp:
cpp
#include "downloader.h"
#include <QDebug>
#include <QFileInfo>
#include <QDir>
Downloader::Downloader(QObject *parent)
: QObject(parent)
, m_manager(new QNetworkAccessManager(this))
, m_reply(nullptr)
, m_file(nullptr)
, m_downloadedBytes(0)
, m_totalBytes(0)
, m_lastBytes(0)
, m_downloading(false)
, m_paused(false)
{
m_speedUpdateTimer = new QTimer(this);
connect(m_speedUpdateTimer, &QTimer::timeout, this, &Downloader::updateSpeed);
}
Downloader::~Downloader()
{
cancel();
}
void Downloader::download(const QString &url, const QString &savePath)
{
if (m_downloading) {
qDebug() << "已有下载任务在进行中";
return;
}
m_url = url;
m_savePath = savePath;
m_tempPath = savePath + ".part";
// 检查是否有未完成的下载
QFileInfo tempFile(m_tempPath);
qint64 startByte = 0;
if (tempFile.exists()) {
startByte = tempFile.size();
m_downloadedBytes = startByte;
qDebug() << "发现未完成下载,已下载:" << formatSize(startByte);
} else {
m_downloadedBytes = 0;
}
startDownload(startByte);
}
void Downloader::startDownload(qint64 startByte)
{
// 打开文件(追加模式)
m_file = new QFile(m_tempPath, this);
QIODevice::OpenMode mode = startByte > 0 ?
QIODevice::Append : QIODevice::WriteOnly;
if (!m_file->open(mode)) {
emit error("无法创建文件: " + m_file->errorString());
return;
}
// 创建请求
QNetworkRequest request(QUrl(m_url));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
// 设置断点续传
if (startByte > 0) {
QString range = QString("bytes=%1-").arg(startByte);
request.setRawHeader("Range", range.toUtf8());
qDebug() << "断点续传,从字节" << startByte << "开始";
}
// 发送请求
m_reply = m_manager->get(request);
connect(m_reply, &QNetworkReply::readyRead,
this, &Downloader::onReadyRead);
connect(m_reply, &QNetworkReply::finished,
this, &Downloader::onFinished);
connect(m_reply, &QNetworkReply::errorOccurred,
this, &Downloader::onError);
connect(m_reply, &QNetworkReply::downloadProgress,
this, &Downloader::onDownloadProgress);
m_downloading = true;
m_paused = false;
m_lastBytes = m_downloadedBytes;
m_speedTimer.start();
m_speedUpdateTimer->start(1000); // 每秒更新速度
qDebug() << "开始下载:" << m_url;
}
void Downloader::pause()
{
if (!m_downloading || m_paused) return;
m_paused = true;
m_speedUpdateTimer->stop();
if (m_reply) {
m_reply->abort();
}
if (m_file) {
m_file->close();
}
qDebug() << "下载已暂停,已下载:" << formatSize(m_downloadedBytes);
emit paused();
}
void Downloader::resume()
{
if (!m_paused) return;
m_paused = false;
qDebug() << "继续下载...";
startDownload(m_downloadedBytes);
emit resumed();
}
void Downloader::cancel()
{
m_speedUpdateTimer->stop();
if (m_reply) {
m_reply->abort();
m_reply->deleteLater();
m_reply = nullptr;
}
if (m_file) {
m_file->close();
m_file->remove(); // 删除临时文件
delete m_file;
m_file = nullptr;
}
m_downloading = false;
m_paused = false;
m_downloadedBytes = 0;
m_totalBytes = 0;
qDebug() << "下载已取消";
}
void Downloader::onReadyRead()
{
if (m_file && m_reply) {
QByteArray data = m_reply->readAll();
m_file->write(data);
m_downloadedBytes += data.size();
}
}
void Downloader::onFinished()
{
m_speedUpdateTimer->stop();
if (!m_reply) return;
int statusCode = m_reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 200: 完整下载, 206: 部分内容(断点续传)
if (m_reply->error() == QNetworkReply::NoError ||
statusCode == 200 || statusCode == 206) {
if (m_file) {
m_file->close();
}
// 重命名临时文件为最终文件
QFile::remove(m_savePath);
if (QFile::rename(m_tempPath, m_savePath)) {
qDebug() << "\n✅ 下载完成:" << m_savePath;
qDebug() << " 文件大小:" << formatSize(m_downloadedBytes);
emit finished(m_savePath);
} else {
emit error("无法重命名文件");
}
}
m_reply->deleteLater();
m_reply = nullptr;
m_downloading = false;
if (m_file) {
delete m_file;
m_file = nullptr;
}
}
void Downloader::onError(QNetworkReply::NetworkError code)
{
if (code == QNetworkReply::OperationCanceledError && m_paused) {
return; // 暂停导致的取消,忽略
}
QString errorString = m_reply ? m_reply->errorString() : "未知错误";
qDebug() << "下载错误:" << errorString;
emit error(errorString);
}
void Downloader::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
// bytesTotal是剩余大小(断点续传时),需要加上已下载部分
if (m_totalBytes == 0 && bytesTotal > 0) {
m_totalBytes = bytesTotal + (m_downloadedBytes - bytesReceived);
}
}
void Downloader::updateSpeed()
{
qint64 elapsed = m_speedTimer.elapsed();
if (elapsed <= 0) return;
qint64 bytesThisInterval = m_downloadedBytes - m_lastBytes;
double speed = bytesThisInterval * 1000.0 / elapsed; // bytes/sec
m_lastBytes = m_downloadedBytes;
m_speedTimer.restart();
// 计算剩余时间
int remaining = 0;
if (speed > 0 && m_totalBytes > m_downloadedBytes) {
remaining = (m_totalBytes - m_downloadedBytes) / speed;
}
emit progressUpdated(m_downloadedBytes, m_totalBytes, speed, remaining);
// 打印进度
double percent = m_totalBytes > 0 ?
(m_downloadedBytes * 100.0 / m_totalBytes) : 0;
QString progress = QString("\r📥 %1% | %2/%3 | %4/s | 剩余 %5")
.arg(percent, 5, 'f', 1)
.arg(formatSize(m_downloadedBytes))
.arg(formatSize(m_totalBytes))
.arg(formatSize(speed))
.arg(formatTime(remaining));
printf("%s", progress.toUtf8().constData());
fflush(stdout);
}
QString Downloader::formatSize(qint64 bytes)
{
if (bytes < 1024) {
return QString("%1 B").arg(bytes);
} else if (bytes < 1024 * 1024) {
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 1);
} else if (bytes < 1024 * 1024 * 1024) {
return QString("%1 MB").arg(bytes / 1024.0 / 1024.0, 0, 'f', 2);
} else {
return QString("%1 GB").arg(bytes / 1024.0 / 1024.0 / 1024.0, 0, 'f', 2);
}
}
QString Downloader::formatTime(int seconds)
{
if (seconds < 60) {
return QString("%1秒").arg(seconds);
} else if (seconds < 3600) {
return QString("%1分%2秒").arg(seconds / 60).arg(seconds % 60);
} else {
return QString("%1时%2分").arg(seconds / 3600).arg((seconds % 3600) / 60);
}
}
main.cpp:
cpp
#include <QCoreApplication>
#include <QTextStream>
#include <QSocketNotifier>
#include <QTimer>
#include "downloader.h"
class DownloadController : public QObject
{
Q_OBJECT
public:
DownloadController(const QString &url, const QString &savePath, QObject *parent = nullptr)
: QObject(parent), m_url(url), m_savePath(savePath)
{
m_downloader = new Downloader(this);
connect(m_downloader, &Downloader::finished, this, [](const QString &path) {
qDebug() << "";
QCoreApplication::quit();
});
connect(m_downloader, &Downloader::error, this, [](const QString &err) {
qDebug() << "\n❌ 错误:" << err;
QCoreApplication::exit(1);
});
// 监听标准输入
m_stdin = new QTextStream(stdin);
m_notifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this, &DownloadController::onInput);
}
void start()
{
printHelp();
m_downloader->download(m_url, m_savePath);
}
private slots:
void onInput()
{
QString cmd = m_stdin->readLine().trimmed().toLower();
if (cmd == "p" || cmd == "pause") {
m_downloader->pause();
}
else if (cmd == "r" || cmd == "resume") {
m_downloader->resume();
}
else if (cmd == "c" || cmd == "cancel") {
m_downloader->cancel();
QCoreApplication::quit();
}
else if (cmd == "h" || cmd == "help") {
printHelp();
}
else if (cmd == "q" || cmd == "quit") {
m_downloader->cancel();
QCoreApplication::quit();
}
}
private:
void printHelp()
{
qDebug() << "========================================";
qDebug() << " HTTP文件下载器";
qDebug() << " 命令: p(暂停) r(继续) c(取消) q(退出)";
qDebug() << "========================================";
}
Downloader *m_downloader;
QTextStream *m_stdin;
QSocketNotifier *m_notifier;
QString m_url;
QString m_savePath;
};
#include "main.moc"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
if (argc < 2) {
qDebug() << "用法: FileDownloader <URL> [保存路径]";
qDebug() << "示例: FileDownloader https://example.com/file.zip";
return 1;
}
QString url = argv[1];
QString savePath;
if (argc >= 3) {
savePath = argv[2];
} else {
// 从URL提取文件名
QUrl qurl(url);
QString fileName = qurl.fileName();
if (fileName.isEmpty()) {
fileName = "download";
}
savePath = QDir::currentPath() + "/" + fileName;
}
qDebug() << "下载:" << url;
qDebug() << "保存到:" << savePath;
DownloadController controller(url, savePath);
controller.start();
return a.exec();
}
7.2.4 编译和运行
bash
# 编译
qmake && make
# 运行
./FileDownloader https://releases.ubuntu.com/22.04/ubuntu-22.04-desktop-amd64.iso
# 运行效果
========================================
HTTP文件下载器
命令: p(暂停) r(继续) c(取消) q(退出)
========================================
下载: https://releases.ubuntu.com/22.04/ubuntu-22.04-desktop-amd64.iso
保存到: /home/user/ubuntu-22.04-desktop-amd64.iso
开始下载...
📥 15.3% | 589.2 MB/3.85 GB | 12.5 MB/s | 剩余 4分32秒
# 按 p 暂停
下载已暂停,已下载: 589.2 MB
# 按 r 继续
继续下载...
断点续传,从字节 617832448 开始
📥 16.1% | 620.8 MB/3.85 GB | 10.2 MB/s | 剩余 5分18秒
# 下载完成
✅ 下载完成: /home/user/ubuntu-22.04-desktop-amd64.iso
文件大小: 3.85 GB
7.3 项目三:局域网设备发现
7.3.1 项目概述
一个基于UDP广播的局域网设备发现工具,包含设备端(响应发现请求)和发现端(搜索设备)。
功能特点:
- UDP广播发现局域网内设备
- 设备信息展示(名称、IP、端口、类型)
- 支持多设备同时在线
- 自动刷新设备列表
7.3.2 项目结构
LANDiscovery/
├── DeviceResponder/
│ ├── DeviceResponder.pro
│ ├── main.cpp
│ ├── deviceresponder.h
│ └── deviceresponder.cpp
└── DeviceDiscoverer/
├── DeviceDiscoverer.pro
├── main.cpp
├── devicediscoverer.h
└── devicediscoverer.cpp
7.3.3 设备响应端完整代码
qmake
QT += core network
QT -= gui
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = DeviceResponder
TEMPLATE = app
SOURCES += main.cpp deviceresponder.cpp
HEADERS += deviceresponder.h
deviceresponder.h:
cpp
#ifndef DEVICERESPONDER_H
#define DEVICERESPONDER_H
#include <QObject>
#include <QUdpSocket>
#include <QJsonObject>
#include <QHostInfo>
#include <QNetworkInterface>
class DeviceResponder : public QObject
{
Q_OBJECT
public:
explicit DeviceResponder(quint16 port, QObject *parent = nullptr);
void setDeviceInfo(const QString &name, const QString &type,
quint16 servicePort);
bool start();
void stop();
private slots:
void onReadyRead();
private:
QJsonObject createResponse();
QString getLocalIP();
QUdpSocket *m_socket;
quint16 m_port;
QString m_deviceName;
QString m_deviceType;
quint16 m_servicePort;
static const QString DISCOVERY_MAGIC;
};
#endif // DEVICERESPONDER_H
deviceresponder.cpp:
cpp
#include "deviceresponder.h"
#include <QDebug>
#include <QJsonDocument>
#include <QDateTime>
#include <QCoreApplication>
const QString DeviceResponder::DISCOVERY_MAGIC = "DEVICE_DISCOVERY_V1";
DeviceResponder::DeviceResponder(quint16 port, QObject *parent)
: QObject(parent)
, m_socket(new QUdpSocket(this))
, m_port(port)
, m_deviceName(QHostInfo::localHostName())
, m_deviceType("generic")
, m_servicePort(0)
{
connect(m_socket, &QUdpSocket::readyRead,
this, &DeviceResponder::onReadyRead);
}
void DeviceResponder::setDeviceInfo(const QString &name, const QString &type,
quint16 servicePort)
{
m_deviceName = name;
m_deviceType = type;
m_servicePort = servicePort;
}
bool DeviceResponder::start()
{
if (m_socket->bind(m_port, QUdpSocket::ShareAddress |
QUdpSocket::ReuseAddressHint)) {
qDebug() << "========================================";
qDebug() << " 设备响应服务已启动";
qDebug() << " 设备名称:" << m_deviceName;
qDebug() << " 设备类型:" << m_deviceType;
qDebug() << " 服务端口:" << m_servicePort;
qDebug() << " 监听端口:" << m_port;
qDebug() << " 本机IP:" << getLocalIP();
qDebug() << "========================================";
return true;
}
qDebug() << "启动失败:" << m_socket->errorString();
return false;
}
void DeviceResponder::stop()
{
m_socket->close();
qDebug() << "设备响应服务已停止";
}
void DeviceResponder::onReadyRead()
{
while (m_socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_socket->pendingDatagramSize());
QHostAddress senderAddress;
quint16 senderPort;
m_socket->readDatagram(datagram.data(), datagram.size(),
&senderAddress, &senderPort);
QString message = QString::fromUtf8(datagram);
// 检查是否是发现请求
if (message.startsWith(DISCOVERY_MAGIC)) {
qDebug() << "收到发现请求 from"
<< senderAddress.toString() << ":" << senderPort;
// 发送响应
QJsonObject response = createResponse();
QByteArray responseData = QJsonDocument(response).toJson(
QJsonDocument::Compact);
m_socket->writeDatagram(responseData, senderAddress, senderPort);
qDebug() << "已发送响应";
}
}
}
QJsonObject DeviceResponder::createResponse()
{
QJsonObject response;
response["magic"] = DISCOVERY_MAGIC;
response["name"] = m_deviceName;
response["type"] = m_deviceType;
response["ip"] = getLocalIP();
response["port"] = m_servicePort;
response["timestamp"] = QDateTime::currentMSecsSinceEpoch();
response["platform"] = QSysInfo::prettyProductName();
return response;
}
QString DeviceResponder::getLocalIP()
{
const auto interfaces = QNetworkInterface::allInterfaces();
for (const QNetworkInterface &interface : interfaces) {
if (interface.flags().testFlag(QNetworkInterface::IsUp) &&
interface.flags().testFlag(QNetworkInterface::IsRunning) &&
!interface.flags().testFlag(QNetworkInterface::IsLoopBack)) {
const auto entries = interface.addressEntries();
for (const QNetworkAddressEntry &entry : entries) {
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) {
return entry.ip().toString();
}
}
}
}
return "127.0.0.1";
}
main.cpp(设备响应端):
cpp
#include <QCoreApplication>
#include "deviceresponder.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
quint16 discoveryPort = 45454;
QString deviceName = "MyDevice";
QString deviceType = "computer";
quint16 servicePort = 8080;
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
QString arg = argv[i];
if (arg == "-n" && i + 1 < argc) {
deviceName = argv[++i];
} else if (arg == "-t" && i + 1 < argc) {
deviceType = argv[++i];
} else if (arg == "-p" && i + 1 < argc) {
servicePort = QString(argv[++i]).toUShort();
} else if (arg == "-d" && i + 1 < argc) {
discoveryPort = QString(argv[++i]).toUShort();
} else if (arg == "-h" || arg == "--help") {
qDebug() << "用法: DeviceResponder [选项]";
qDebug() << " -n <名称> 设备名称 (默认: MyDevice)";
qDebug() << " -t <类型> 设备类型 (默认: computer)";
qDebug() << " -p <端口> 服务端口 (默认: 8080)";
qDebug() << " -d <端口> 发现端口 (默认: 45454)";
return 0;
}
}
DeviceResponder responder(discoveryPort);
responder.setDeviceInfo(deviceName, deviceType, servicePort);
if (!responder.start()) {
return 1;
}
return a.exec();
}
7.3.4 设备发现端完整代码
qmake
QT += core network
QT -= gui
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = DeviceDiscoverer
TEMPLATE = app
SOURCES += main.cpp devicediscoverer.cpp
HEADERS += devicediscoverer.h
devicediscoverer.h:
cpp
#ifndef DEVICEDISCOVERER_H
#define DEVICEDISCOVERER_H
#include <QObject>
#include <QUdpSocket>
#include <QTimer>
#include <QJsonObject>
#include <QMap>
struct DeviceInfo {
QString name;
QString type;
QString ip;
quint16 port;
QString platform;
qint64 lastSeen;
};
class DeviceDiscoverer : public QObject
{
Q_OBJECT
public:
explicit DeviceDiscoverer(quint16 port, QObject *parent = nullptr);
void startDiscovery(int intervalMs = 3000);
void stopDiscovery();
void discover();
QList<DeviceInfo> getDevices() const;
signals:
void deviceFound(const DeviceInfo &device);
void deviceLost(const QString &ip);
void discoveryCompleted(int count);
private slots:
void onReadyRead();
void sendDiscoveryRequest();
void cleanupOldDevices();
private:
void printDeviceTable();
QUdpSocket *m_socket;
QTimer *m_discoveryTimer;
QTimer *m_cleanupTimer;
quint16 m_port;
QMap<QString, DeviceInfo> m_devices; // IP -> DeviceInfo
static const QString DISCOVERY_MAGIC;
static const int DEVICE_TIMEOUT_MS = 15000; // 15秒无响应则移除
};
#endif // DEVICEDISCOVERER_H
devicediscoverer.cpp:
cpp
#include "devicediscoverer.h"
#include <QDebug>
#include <QJsonDocument>
#include <QDateTime>
#include <QHostAddress>
#include <QNetworkInterface>
const QString DeviceDiscoverer::DISCOVERY_MAGIC = "DEVICE_DISCOVERY_V1";
DeviceDiscoverer::DeviceDiscoverer(quint16 port, QObject *parent)
: QObject(parent)
, m_socket(new QUdpSocket(this))
, m_port(port)
{
connect(m_socket, &QUdpSocket::readyRead,
this, &DeviceDiscoverer::onReadyRead);
m_discoveryTimer = new QTimer(this);
connect(m_discoveryTimer, &QTimer::timeout,
this, &DeviceDiscoverer::sendDiscoveryRequest);
m_cleanupTimer = new QTimer(this);
connect(m_cleanupTimer, &QTimer::timeout,
this, &DeviceDiscoverer::cleanupOldDevices);
// 绑定随机端口接收响应
m_socket->bind(QHostAddress::Any, 0);
}
void DeviceDiscoverer::startDiscovery(int intervalMs)
{
qDebug() << "========================================";
qDebug() << " 局域网设备发现工具";
qDebug() << " 目标端口:" << m_port;
qDebug() << " 刷新间隔:" << intervalMs / 1000 << "秒";
qDebug() << "========================================\n";
m_discoveryTimer->start(intervalMs);
m_cleanupTimer->start(5000); // 每5秒清理一次
// 立即发送一次
sendDiscoveryRequest();
}
void DeviceDiscoverer::stopDiscovery()
{
m_discoveryTimer->stop();
m_cleanupTimer->stop();
qDebug() << "发现服务已停止";
}
void DeviceDiscoverer::discover()
{
sendDiscoveryRequest();
}
QList<DeviceInfo> DeviceDiscoverer::getDevices() const
{
return m_devices.values();
}
void DeviceDiscoverer::sendDiscoveryRequest()
{
QByteArray request = (DISCOVERY_MAGIC + ":DISCOVER").toUtf8();
// 发送广播
m_socket->writeDatagram(request, QHostAddress::Broadcast, m_port);
qDebug() << "📡 发送发现广播...";
}
void DeviceDiscoverer::onReadyRead()
{
while (m_socket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_socket->pendingDatagramSize());
QHostAddress senderAddress;
quint16 senderPort;
m_socket->readDatagram(datagram.data(), datagram.size(),
&senderAddress, &senderPort);
// 解析JSON响应
QJsonDocument doc = QJsonDocument::fromJson(datagram);
if (!doc.isObject()) continue;
QJsonObject obj = doc.object();
// 验证魔数
if (obj["magic"].toString() != DISCOVERY_MAGIC) continue;
QString ip = obj["ip"].toString();
if (ip.isEmpty()) ip = senderAddress.toString();
DeviceInfo device;
device.name = obj["name"].toString();
device.type = obj["type"].toString();
device.ip = ip;
device.port = obj["port"].toInt();
device.platform = obj["platform"].toString();
device.lastSeen = QDateTime::currentMSecsSinceEpoch();
bool isNew = !m_devices.contains(ip);
m_devices[ip] = device;
if (isNew) {
qDebug() << "✅ 发现新设备:" << device.name
<< "(" << device.ip << ")";
emit deviceFound(device);
}
}
// 打印设备表格
printDeviceTable();
emit discoveryCompleted(m_devices.size());
}
void DeviceDiscoverer::cleanupOldDevices()
{
qint64 now = QDateTime::currentMSecsSinceEpoch();
QStringList toRemove;
for (auto it = m_devices.begin(); it != m_devices.end(); ++it) {
if (now - it.value().lastSeen > DEVICE_TIMEOUT_MS) {
toRemove.append(it.key());
}
}
for (const QString &ip : toRemove) {
QString name = m_devices[ip].name;
m_devices.remove(ip);
qDebug() << "❌ 设备离线:" << name << "(" << ip << ")";
emit deviceLost(ip);
}
}
void DeviceDiscoverer::printDeviceTable()
{
if (m_devices.isEmpty()) {
qDebug() << "未发现任何设备\n";
return;
}
qDebug() << "\n┌─────────────────────────────────────────────────────────────────┐";
qDebug() << "│ 设备名称 │ IP地址 │ 端口 │ 类型 │";
qDebug() << "├─────────────────────────────────────────────────────────────────┤";
for (const DeviceInfo &device : m_devices) {
QString line = QString("│ %-17s │ %-15s │ %-5d │ %-10s │")
.arg(device.name.left(17))
.arg(device.ip)
.arg(device.port)
.arg(device.type.left(10));
qDebug().noquote() << line;
}
qDebug() << "└─────────────────────────────────────────────────────────────────┘";
qDebug() << "在线设备:" << m_devices.size() << "台\n";
}
main.cpp(设备发现端):
cpp
#include <QCoreApplication>
#include <QTextStream>
#include <QSocketNotifier>
#include "devicediscoverer.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
quint16 discoveryPort = 45454;
int interval = 5000; // 5秒
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
QString arg = argv[i];
if (arg == "-p" && i + 1 < argc) {
discoveryPort = QString(argv[++i]).toUShort();
} else if (arg == "-i" && i + 1 < argc) {
interval = QString(argv[++i]).toInt() * 1000;
} else if (arg == "-h" || arg == "--help") {
qDebug() << "用法: DeviceDiscoverer [选项]";
qDebug() << " -p <端口> 发现端口 (默认: 45454)";
qDebug() << " -i <秒> 刷新间隔 (默认: 5)";
return 0;
}
}
DeviceDiscoverer discoverer(discoveryPort);
// 监听键盘输入
QTextStream stdin(::stdin);
QSocketNotifier notifier(fileno(::stdin), QSocketNotifier::Read);
QObject::connect(¬ifier, &QSocketNotifier::activated, [&]() {
QString cmd = stdin.readLine().trimmed().toLower();
if (cmd == "r" || cmd == "refresh") {
discoverer.discover();
} else if (cmd == "l" || cmd == "list") {
auto devices = discoverer.getDevices();
qDebug() << "\n当前设备列表:";
for (const auto &d : devices) {
qDebug() << " -" << d.name << "(" << d.ip << ":" << d.port << ")";
}
} else if (cmd == "q" || cmd == "quit") {
QCoreApplication::quit();
} else if (cmd == "h" || cmd == "help") {
qDebug() << "命令: r(立即刷新) l(列表) q(退出) h(帮助)";
}
});
qDebug() << "命令: r(立即刷新) l(列表) q(退出) h(帮助)\n";
discoverer.startDiscovery(interval);
return a.exec();
}
7.3.5 编译和运行
bash
# 编译设备响应端
cd DeviceResponder
qmake && make
# 编译设备发现端
cd ../DeviceDiscoverer
qmake && make
# 运行设备响应端(在不同电脑或终端运行多个实例)
./DeviceResponder -n "客厅电脑" -t computer -p 8080
./DeviceResponder -n "办公室打印机" -t printer -p 9100
./DeviceResponder -n "智能电视" -t tv -p 8443
# 运行设备发现端
./DeviceDiscoverer
# 运行效果
========================================
局域网设备发现工具
目标端口: 45454
刷新间隔: 5秒
========================================
命令: r(立即刷新) l(列表) q(退出) h(帮助)
📡 发送发现广播...
✅ 发现新设备: 客厅电脑 (192.168.1.100)
✅ 发现新设备: 办公室打印机 (192.168.1.101)
✅ 发现新设备: 智能电视 (192.168.1.102)
┌─────────────────────────────────────────────────────────────────┐
│ 设备名称 │ IP地址 │ 端口 │ 类型 │
├─────────────────────────────────────────────────────────────────┤
│ 客厅电脑 │ 192.168.1.100 │ 8080 │ computer │
│ 办公室打印机 │ 192.168.1.101 │ 9100 │ printer │
│ 智能电视 │ 192.168.1.102 │ 8443 │ tv │
└─────────────────────────────────────────────────────────────────┘
在线设备: 3台
第七章小结
本章通过三个完整的实战项目,涵盖了Qt网络编程的主要应用场景:
| 项目 | 技术栈 | 应用场景 |
|---|---|---|
| TCP聊天程序 | QTcpServer + QTcpSocket | 即时通讯、游戏服务器 |
| HTTP文件下载器 | QNetworkAccessManager | 文件传输、更新下载 |
| 局域网设备发现 | QUdpSocket + 广播 | 智能家居、局域网工具 |
每个项目都可以作为实际应用的基础进行扩展:
- 聊天程序可加入私聊、文件传输、历史记录
- 下载器可加入多线程分段下载、下载队列
- 设备发现可加入设备控制、服务调用
📋 第八章:进阶主题
本章介绍Qt网络编程的进阶技术,帮助你构建更专业的网络应用。
8.1 异步DNS解析
8.1.1 QHostInfo异步查询
cpp
#include <QHostInfo>
class DnsResolver : public QObject
{
Q_OBJECT
public:
void resolve(const QString &hostname) {
// 异步DNS查询
int lookupId = QHostInfo::lookupHost(hostname,
this, SLOT(onLookupFinished(QHostInfo)));
qDebug() << "开始解析:" << hostname << "ID:" << lookupId;
}
// 取消查询
void cancel(int lookupId) {
QHostInfo::abortHostLookup(lookupId);
}
private slots:
void onLookupFinished(const QHostInfo &host) {
if (host.error() != QHostInfo::NoError) {
qDebug() << "DNS解析失败:" << host.errorString();
return;
}
qDebug() << "域名:" << host.hostName();
const auto addresses = host.addresses();
for (const QHostAddress &addr : addresses) {
if (addr.protocol() == QAbstractSocket::IPv4Protocol) {
qDebug() << " IPv4:" << addr.toString();
} else if (addr.protocol() == QAbstractSocket::IPv6Protocol) {
qDebug() << " IPv6:" << addr.toString();
}
}
}
};
// 同步查询(会阻塞)
void syncLookup(const QString &hostname) {
QHostInfo info = QHostInfo::fromName(hostname);
// 注意:这会阻塞当前线程!
}
8.1.2 DNS缓存管理
cpp
class DnsCache {
public:
static DnsCache& instance() {
static DnsCache cache;
return cache;
}
QHostAddress lookup(const QString &hostname) {
QMutexLocker locker(&m_mutex);
auto it = m_cache.find(hostname);
if (it != m_cache.end()) {
// 检查是否过期(5分钟TTL)
if (it->timestamp.secsTo(QDateTime::currentDateTime()) < 300) {
return it->address;
}
}
return QHostAddress();
}
void store(const QString &hostname, const QHostAddress &address) {
QMutexLocker locker(&m_mutex);
m_cache[hostname] = {address, QDateTime::currentDateTime()};
}
void clear() {
QMutexLocker locker(&m_mutex);
m_cache.clear();
}
private:
struct CacheEntry {
QHostAddress address;
QDateTime timestamp;
};
QMap<QString, CacheEntry> m_cache;
QMutex m_mutex;
};
8.2 代理服务器配置
8.2.1 设置HTTP/SOCKS代理
cpp
#include <QNetworkProxy>
class ProxyManager {
public:
// 设置HTTP代理
static void setHttpProxy(const QString &host, quint16 port,
const QString &user = QString(),
const QString &password = QString()) {
QNetworkProxy proxy;
proxy.setType(QNetworkProxy::HttpProxy);
proxy.setHostName(host);
proxy.setPort(port);
if (!user.isEmpty()) {
proxy.setUser(user);
proxy.setPassword(password);
}
QNetworkProxy::setApplicationProxy(proxy);
qDebug() << "已设置HTTP代理:" << host << ":" << port;
}
// 设置SOCKS5代理
static void setSocks5Proxy(const QString &host, quint16 port,
const QString &user = QString(),
const QString &password = QString()) {
QNetworkProxy proxy;
proxy.setType(QNetworkProxy::Socks5Proxy);
proxy.setHostName(host);
proxy.setPort(port);
if (!user.isEmpty()) {
proxy.setUser(user);
proxy.setPassword(password);
}
QNetworkProxy::setApplicationProxy(proxy);
qDebug() << "已设置SOCKS5代理:" << host << ":" << port;
}
// 使用系统代理
static void useSystemProxy() {
QNetworkProxyFactory::setUseSystemConfiguration(true);
qDebug() << "使用系统代理设置";
}
// 禁用代理
static void noProxy() {
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
qDebug() << "已禁用代理";
}
// 为特定请求设置代理
static void setProxyForRequest(QNetworkRequest &request,
const QNetworkProxy &proxy) {
// Qt 5.9+
request.setAttribute(QNetworkRequest::ProxyAttribute,
QVariant::fromValue(proxy));
}
};
// 使用示例
void configureProxy() {
// 方式1:全局代理
ProxyManager::setHttpProxy("127.0.0.1", 8080);
// 方式2:单个请求代理
QNetworkRequest request(QUrl("https://api.example.com"));
QNetworkProxy proxy(QNetworkProxy::HttpProxy, "proxy.local", 3128);
request.setAttribute(QNetworkRequest::ProxyAttribute,
QVariant::fromValue(proxy));
}
8.2.2 代理自动配置(PAC)
cpp
class PacProxyFactory : public QNetworkProxyFactory
{
public:
PacProxyFactory(const QString &pacUrl) : m_pacUrl(pacUrl) {
downloadPac();
}
QList<QNetworkProxy> queryProxy(const QNetworkProxyQuery &query) override {
QString host = query.peerHostName();
// 简化的PAC解析逻辑
if (shouldBypass(host)) {
return {QNetworkProxy::NoProxy};
}
return {m_proxy};
}
private:
void downloadPac() {
// 下载并解析PAC文件
// 实际实现需要JavaScript引擎解析PAC脚本
}
bool shouldBypass(const QString &host) {
// 检查是否应该绑过代理
return host == "localhost" ||
host.endsWith(".local") ||
host.startsWith("192.168.");
}
QString m_pacUrl;
QNetworkProxy m_proxy;
};
// 安装自定义代理工厂
void installPacProxy() {
QNetworkProxyFactory::setApplicationProxyFactory(
new PacProxyFactory("http://proxy.example.com/proxy.pac")
);
}
8.3 网络状态监测
8.3.1 使用QNetworkInformation (Qt 6.1+)
cpp
#include <QNetworkInformation>
class NetworkMonitor : public QObject
{
Q_OBJECT
public:
NetworkMonitor(QObject *parent = nullptr) : QObject(parent) {
// 加载网络信息后端
if (!QNetworkInformation::loadDefaultBackend()) {
qWarning() << "无法加载网络信息后端";
return;
}
auto *netInfo = QNetworkInformation::instance();
if (!netInfo) return;
// 监听可达性变化
connect(netInfo, &QNetworkInformation::reachabilityChanged,
this, &NetworkMonitor::onReachabilityChanged);
// 监听传输介质变化
connect(netInfo, &QNetworkInformation::transportMediumChanged,
this, &NetworkMonitor::onTransportChanged);
// 初始状态
printStatus();
}
bool isOnline() const {
auto *netInfo = QNetworkInformation::instance();
if (!netInfo) return true; // 假设在线
return netInfo->reachability() ==
QNetworkInformation::Reachability::Online;
}
private slots:
void onReachabilityChanged(QNetworkInformation::Reachability reachability) {
QString status;
switch (reachability) {
case QNetworkInformation::Reachability::Unknown:
status = "未知";
break;
case QNetworkInformation::Reachability::Disconnected:
status = "断开连接";
emit networkLost();
break;
case QNetworkInformation::Reachability::Local:
status = "仅本地网络";
break;
case QNetworkInformation::Reachability::Site:
status = "仅局域网";
break;
case QNetworkInformation::Reachability::Online:
status = "在线";
emit networkRestored();
break;
}
qDebug() << "🌐 网络状态:" << status;
}
void onTransportChanged(QNetworkInformation::TransportMedium medium) {
QString type;
switch (medium) {
case QNetworkInformation::TransportMedium::Ethernet:
type = "有线网络";
break;
case QNetworkInformation::TransportMedium::WiFi:
type = "WiFi";
break;
case QNetworkInformation::TransportMedium::Cellular:
type = "蜂窝网络";
break;
default:
type = "其他";
}
qDebug() << "📡 传输介质:" << type;
}
signals:
void networkLost();
void networkRestored();
private:
void printStatus() {
auto *netInfo = QNetworkInformation::instance();
if (!netInfo) return;
qDebug() << "========================================";
qDebug() << " 网络状态监测已启动";
qDebug() << " 后端:" << netInfo->backendName();
onReachabilityChanged(netInfo->reachability());
onTransportChanged(netInfo->transportMedium());
qDebug() << "========================================";
}
};
8.3.2 网络连通性检测
cpp
class ConnectivityChecker : public QObject
{
Q_OBJECT
public:
ConnectivityChecker(QObject *parent = nullptr) : QObject(parent) {
m_manager = new QNetworkAccessManager(this);
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &ConnectivityChecker::check);
}
void startMonitoring(int intervalMs = 30000) {
m_timer->start(intervalMs);
check(); // 立即检测一次
}
void check() {
// 检测多个可靠的服务
QStringList urls = {
"https://www.google.com/generate_204",
"https://www.apple.com/library/test/success.html",
"https://connectivity-check.ubuntu.com"
};
m_pendingChecks = urls.size();
m_successCount = 0;
for (const QString &url : urls) {
QNetworkRequest request(QUrl(url));
request.setTransferTimeout(5000);
QNetworkReply *reply = m_manager->head(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
m_successCount++;
}
reply->deleteLater();
m_pendingChecks--;
if (m_pendingChecks == 0) {
bool online = m_successCount > 0;
if (online != m_lastOnline) {
m_lastOnline = online;
emit connectivityChanged(online);
qDebug() << "网络连通性:" << (online ? "在线" : "离线");
}
}
});
}
}
signals:
void connectivityChanged(bool online);
private:
QNetworkAccessManager *m_manager;
QTimer *m_timer;
int m_pendingChecks = 0;
int m_successCount = 0;
bool m_lastOnline = true;
};
8.4 Qt 6网络新特性
8.4.1 QNetworkInformation
Qt 6.1引入了QNetworkInformation类,提供系统级网络状态信息:
cpp
// 支持的功能查询
auto features = QNetworkInformation::supportedFeatures();
if (features.testFlag(QNetworkInformation::Feature::Reachability)) {
qDebug() << "支持可达性检测";
}
if (features.testFlag(QNetworkInformation::Feature::CaptivePortal)) {
qDebug() << "支持强制门户检测";
}
if (features.testFlag(QNetworkInformation::Feature::TransportMedium)) {
qDebug() << "支持传输介质检测";
}
if (features.testFlag(QNetworkInformation::Feature::Metered)) {
qDebug() << "支持计量网络检测";
}
8.4.2 HTTP/2支持
Qt 6默认启用HTTP/2:
cpp
// 检查是否使用了HTTP/2
connect(reply, &QNetworkReply::finished, [reply]() {
// 获取HTTP版本
QVariant httpVersion = reply->attribute(
QNetworkRequest::Http2WasUsedAttribute);
if (httpVersion.toBool()) {
qDebug() << "使用HTTP/2协议";
} else {
qDebug() << "使用HTTP/1.1协议";
}
});
// 强制使用HTTP/1.1(如果需要)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
8.4.3 改进的SSL/TLS
cpp
// Qt 6中的SSL配置
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
// 设置最低TLS版本
sslConfig.setProtocol(QSsl::TlsV1_2OrLater);
// ALPN协议协商
sslConfig.setAllowedNextProtocols({
QSslConfiguration::NextProtocolHttp1_1,
QSslConfiguration::ALPNProtocolHTTP2
});
// 设置会话票证(Session Tickets)
sslConfig.setSessionTickets({}); // 使用默认
request.setSslConfiguration(sslConfig);
8.5 性能调优
8.5.1 连接池管理
cpp
class ConnectionPool {
public:
static QNetworkAccessManager* getManager() {
// 线程局部单例
thread_local QNetworkAccessManager manager;
// 配置连接限制
// 注意:这些是Qt内部设置,可能需要通过环境变量配置
// 例如:QT_HTTP_MAX_CONNECTIONS_PER_HOST
return &manager;
}
static void configure() {
// 通过环境变量配置
qputenv("QT_HTTP_MAX_CONNECTIONS_PER_HOST", "6");
}
};
8.5.2 请求优化
cpp
class OptimizedRequest {
public:
static QNetworkRequest create(const QUrl &url) {
QNetworkRequest request(url);
// 启用HTTP/2
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
// 启用管道(HTTP/1.1)
request.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
// 设置超时
request.setTransferTimeout(30000);
// 启用压缩
request.setRawHeader("Accept-Encoding", "gzip, deflate");
// 缓存策略
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
QNetworkRequest::PreferCache);
return request;
}
};
8.5.3 大文件传输优化
cpp
class LargeFileTransfer : public QObject
{
Q_OBJECT
public:
void uploadLargeFile(const QString &url, const QString &filePath) {
QFile *file = new QFile(filePath, this);
if (!file->open(QIODevice::ReadOnly)) {
emit error("无法打开文件");
return;
}
QNetworkRequest request(QUrl(url));
request.setHeader(QNetworkRequest::ContentLengthHeader, file->size());
// 分块传输
request.setRawHeader("Transfer-Encoding", "chunked");
QNetworkReply *reply = m_manager->post(request, file);
file->setParent(reply); // reply完成后自动删除file
connect(reply, &QNetworkReply::uploadProgress,
this, [this](qint64 sent, qint64 total) {
double percent = total > 0 ? (sent * 100.0 / total) : 0;
qDebug() << QString("上传进度: %1%").arg(percent, 0, 'f', 1);
});
}
signals:
void error(const QString &msg);
private:
QNetworkAccessManager *m_manager = new QNetworkAccessManager(this);
};
第八章小结
本章介绍了Qt网络编程的进阶主题:
| 主题 | 关键类/技术 |
|---|---|
| 异步DNS | QHostInfo::lookupHost() |
| 代理配置 | QNetworkProxy, QNetworkProxyFactory |
| 网络监测 | QNetworkInformation (Qt 6.1+) |
| Qt 6新特性 | HTTP/2, 改进的SSL/TLS |
| 性能调优 | 连接池、请求优化、分块传输 |
附录
A. Qt网络模块API速查表
A.1 核心类
| 类名 | 用途 | 常用方法 |
|---|---|---|
| QTcpServer | TCP服务器 | listen(), nextPendingConnection() |
| QTcpSocket | TCP客户端 | connectToHost(), write(), read() |
| QUdpSocket | UDP通信 | bind(), writeDatagram(), readDatagram() |
| QNetworkAccessManager | HTTP客户端 | get(), post(), put(), delete() |
| QNetworkRequest | HTTP请求 | setUrl(), setHeader(), setRawHeader() |
| QNetworkReply | HTTP响应 | readAll(), error(), attribute() |
| QWebSocket | WebSocket客户端 | open(), sendTextMessage(), close() |
| QWebSocketServer | WebSocket服务器 | listen(), nextPendingConnection() |
A.2 常用信号
cpp
// QTcpSocket / QUdpSocket
void connected();
void disconnected();
void readyRead();
void errorOccurred(QAbstractSocket::SocketError);
void stateChanged(QAbstractSocket::SocketState);
// QNetworkReply
void finished();
void downloadProgress(qint64, qint64);
void uploadProgress(qint64, qint64);
void errorOccurred(QNetworkReply::NetworkError);
void sslErrors(const QList<QSslError>&);
// QWebSocket
void connected();
void disconnected();
void textMessageReceived(const QString&);
void binaryMessageReceived(const QByteArray&);
void pong(quint64, const QByteArray&);
A.3 .pro文件配置
qmake
# TCP/UDP
QT += network
# WebSocket
QT += websockets
# SSL支持(通常自动包含)
# 需确保系统安装OpenSSL
B. 常见网络错误码与解决方案
B.1 QAbstractSocket::SocketError
| 错误码 | 名称 | 原因 | 解决方案 |
|---|---|---|---|
| 0 | ConnectionRefusedError | 目标拒绝连接 | 检查服务是否运行、端口是否正确 |
| 1 | RemoteHostClosedError | 远程主机关闭 | 正常断开或实现重连机制 |
| 2 | HostNotFoundError | 找不到主机 | 检查域名/IP是否正确 |
| 3 | SocketAccessError | 权限不足 | Linux下绑定<1024端口需root |
| 4 | SocketResourceError | 资源不足 | 检查文件描述符限制 |
| 5 | SocketTimeoutError | 连接超时 | 检查网络、增加超时时间 |
B.2 QNetworkReply::NetworkError
| 错误码 | 名称 | HTTP状态 | 解决方案 |
|---|---|---|---|
| 1 | ConnectionRefusedError | - | 检查服务器是否在线 |
| 2 | RemoteHostClosedError | - | 实现重连机制 |
| 3 | HostNotFoundError | - | 检查DNS、网络连接 |
| 4 | TimeoutError | - | 增加超时时间、检查网络 |
| 99 | UnknownNetworkError | - | 查看errorString() |
| 201 | ContentAccessDenied | 403 | 检查权限、认证Token |
| 203 | ContentNotFoundError | 404 | 检查URL是否正确 |
| 204 | AuthenticationRequiredError | 401 | 提供有效认证信息 |
| 302 | InsecureRedirectError | 3xx | 设置重定向策略 |
C. HTTP状态码速查
C.1 成功 (2xx)
| 状态码 | 名称 | 说明 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 成功但无返回内容 |
| 206 | Partial Content | 断点续传返回部分内容 |
C.2 重定向 (3xx)
| 状态码 | 名称 | 说明 |
|---|---|---|
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 资源未修改,使用缓存 |
C.3 客户端错误 (4xx)
| 状态码 | 名称 | 说明 |
|---|---|---|
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | HTTP方法不允许 |
| 429 | Too Many Requests | 请求频率限制 |
C.4 服务端错误 (5xx)
| 状态码 | 名称 | 说明 |
|---|---|---|
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务暂时不可用 |
| 504 | Gateway Timeout | 网关超时 |
D. 网络调试工具推荐
D.1 抓包工具
| 工具 | 平台 | 特点 |
|---|---|---|
| Wireshark | 全平台 | 功能强大,支持所有协议 |
| Fiddler | Windows | HTTP/HTTPS调试专用 |
| Charles | 全平台 | 移动开发友好 |
| mitmproxy | 全平台 | 命令行,可编程 |
D.2 测试工具
| 工具 | 用途 |
|---|---|
| Postman | API测试、HTTP请求 |
| curl | 命令行HTTP客户端 |
| netcat (nc) | TCP/UDP测试 |
| tcpdump | 命令行抓包 |
D.3 在线服务
| 服务 | 地址 | 用途 |
|---|---|---|
| httpbin | httpbin.org | HTTP测试 |
| WebSocket Echo | echo.websocket.org | WebSocket测试 |
| Public APIs | github.com/public-apis | API列表 |
D.4 Qt调试技巧
cpp
// 启用网络调试日志
qputenv("QT_LOGGING_RULES", "qt.network.*=true");
// SSL调试
qputenv("QT_LOGGING_RULES", "qt.network.ssl=true");
// 详细日志
QLoggingCategory::setFilterRules("qt.network.ssl.warning=true\n"
"qt.network.ssl.debug=true");
E. 参考资源
E.1 官方文档
- Qt Network Programming - Qt网络模块官方文档
- Qt WebSockets - WebSocket模块文档
- QNetworkAccessManager - HTTP客户端详解
- QSslConfiguration - SSL配置指南
E.2 示例代码
- Qt Network Examples - 官方网络示例
- Fortune Server/Client - TCP示例
- Broadcast Sender/Receiver - UDP广播示例
E.3 协议规范
- RFC 793 - TCP协议
- RFC 768 - UDP协议
- RFC 7230-7235 - HTTP/1.1
- RFC 7540 - HTTP/2
- RFC 6455 - WebSocket协议
总结
本文档全面介绍了Qt网络编程的各个方面:
知识体系回顾
Qt网络编程
├── 第一章:网络编程基础
│ └── 网络模型、TCP/UDP协议、Qt网络模块概览
├── 第二章:QTcpSocket详解
│ └── TCP客户端/服务端、数据帧设计、多客户端处理
├── 第三章:QUdpSocket详解
│ └── UDP通信、广播、组播、可靠性增强
├── 第四章:HTTP编程
│ └── GET/POST、RESTful API、文件传输、SSL配置
├── 第五章:WebSocket通信
│ └── 客户端/服务端、心跳机制、实时应用
├── 第六章:最佳实践
│ └── 错误处理、超时、重试、性能、安全
├── 第七章:实战项目
│ └── TCP聊天、HTTP下载器、局域网发现
└── 第八章:进阶主题
└── DNS解析、代理、网络监测、Qt6新特性
核心要点
| 协议 | 使用场景 | 关键类 |
|---|---|---|
| TCP | 可靠传输(聊天、文件) | QTcpServer, QTcpSocket |
| UDP | 实时传输(视频、发现) | QUdpSocket |
| HTTP | Web API调用 | QNetworkAccessManager |
| WebSocket | 双向实时通信 | QWebSocket, QWebSocketServer |
开发建议
- 始终使用异步模式 - 通过信号槽处理网络事件
- 妥善处理错误 - 每个网络操作都可能失败
- 设置合理超时 - 避免请求无限等待
- 实现重试机制 - 提高应用健壮性
- 注意安全性 - 使用HTTPS,保护敏感数据
- 测试各种场景 - 弱网、断网、高延迟
文档版本 : 1.0
适用Qt版本 : Qt 5.15+ / Qt 6.x
最后更新: 2026年1月