跳到主要内容C++ Qt 网络编程:QUdpSocket、QTcpSocket 与 Http 实战 | 极客日志C++
C++ Qt 网络编程:QUdpSocket、QTcpSocket 与 Http 实战
综述由AI生成Qt 网络编程基于 Socket API,涵盖 UDP、TCP 及 HTTP 协议实现。核心类包括 QUdpSocket、QTcpServer、QTcpSocket 及 QNetworkAccessManager。文章通过回显服务器和客户端 Demo 演示了绑定端口、信号槽处理 readyRead/newConnection/disconnected 事件、粘包处理注意事项以及 HTTP 请求的异步回调机制。重点在于模块化配置 .pro 文件及正确使用 Qt 的事件驱动模型替代传统循环阻塞。
C++ Qt 网络编程
网络编程主要依赖于操作系统提供的 Socket API。需要注意的是,C++标准库本身并未封装网络编程相关的 API。
Qt 网络编程要点
- 网络应用开发本质上是编写应用层代码,需要传输层协议(如 TCP/UDP)的支持
- Qt 提供了两套专门的网络编程 API(QUdpSocket 和 QTcpSocket)
- 使用 Qt 网络编程 API 时,需先在 .pro 文件中添加 network 模块
- 之前学习的 Qt 控件和核心功能都属于 QtCore 模块(默认已包含)
Qt 模块化处理是为了避免生成过大的可执行程序。默认情况下额外模块不会参与编译,需要在 .pro 文件中引入对应的模块。
1. QUdpSocket
核心 API 概览
主要的类有两个:
QUdpSocket:同 Linux 理解的 socket 概念,本质是打开的一个文件描述符,通过这个文件进行通信。
QNetworkDatagram:Qt 对 UDP 数据报的完整封装,不仅包含二进制数据,还包含了发送/接收的地址端口等元信息。
QUdpSocket
| 名称 | 类型 | 说明 | 对标原生 API |
|---|
| bind(const QHostAddress&, quint16) | 方法 | 绑定指定的端口号 | bind |
| receiveDatagram() | 方法 | 返回 QNetworkDatagram 读取一个 UDP 数据报 | recvfrom |
| writeDatagram(const QNetworkDatagram&) | 方法 | 发送一个 UDP 数据报 | sendto |
| readyRead | 信号 | 在收到数据并准备就绪后触发 | 无 (类似于 IO 多路复用的通知机制) |
QNetworkDatagram
| 名称 | 类型 | 说明 | 对标原生 API |
|---|
| QNetworkDatagram(const QByteArray&, const QHostAddress& , quint16 ) | 构造函数 | 通过 QByteArray , 目标 IP 地址,目标端口号构造一个 UDP 数据报 | 无 |
| data() | 方法 | 获取数据报内部持有的数据 | 无 |
| senderAddress() | 方法 | 获取数据报中包含的对端的 IP 地址 | 无 |
| senderPort() | 方法 | 获取数据报中包含的对端的端口号 | 无 |
实操 Demo
UDP 回显服务器
实现一个带有界面的 UDP 回显服务器。
- 创建 Widget 项目
- 创建一个 QListWidget 表示通过内部的 item 表示若干条消息
- 创建
QUdpSocket 成员变量 socket 句柄
构造函数中初始化 socket 对象,设置窗口标题,连接信号槽接收 readyRead 信号进行绑定端口号(先连接信号槽,再绑定端口)
- socket 成员函数
bind
- 第一个参数 ip(任意 ip 也就是
QHostAddress::Any 宏)
- 第二个参数 端口号
- 检查返回值,若失败则调用
QMessageBox::critical 提示错误
槽函数(processRequest):
- 读取请求并解析:通过
receiveDatagram 获取请求,转换为 QString
- 根据请求计算响应(回显服务器直接返回请求本身)
- 把响应写回给客户端:将 response 转为 QByteArray,设置返回的 IP 和端口(来自
senderAddress、senderPort),调用 writeDatagram
- 把这次交互的信息显示在界面上
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
#include <QHostAddress>
#include <QMessageBox>
#include <QNetworkDatagram>
#include <QDateTime>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
QString process2(const QString& s);
private slots:
void process();
};
#endif
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(parent);
ui->listWidget->setObjectName(QString("UDP 服务器"));
connect(socket, &QUdpSocket::readyRead, this, &Widget::process);
bool isconnect = socket->bind(QHostAddress::Any, 8081);
if (!isconnect) {
QMessageBox::critical(this, "错误", "服务器启动错误");
return;
}
setWindowTitle("Udp 服务端");
}
Widget::~Widget()
{
delete ui;
}
QString Widget::process2(const QString& request)
{
return request;
}
void Widget::process()
{
QNetworkDatagram requestPacket = socket->receiveDatagram();
QString request = requestPacket.data();
QString response = process2(request);
QNetworkDatagram responsePacket(response.toUtf8(), requestPacket.senderAddress(), requestPacket.senderPort());
socket->writeDatagram(responsePacket);
qint64 msTimestamp = QDateTime::currentMSecsSinceEpoch();
QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(msTimestamp);
QString formatted = dateTime.toString("yyyy-MM-dd hh:mm:ss.zzz");
QString log = "[" + formatted + " : " + requestPacket.senderAddress().toString() + ":" + QString::number(requestPacket.senderPort()) + "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
}
UDP 回显客户端
- 打开 ui 文件拖拽(保证可以主动给服务器发起请求)
- 创建 QUdpSocket 类型的成员变量 socket
- 构造函数中添加全局变量 SERVER_IP (127.0.0.1) 和 SERVER_PORT (9090),初始化 socket 对象
- 给按钮添加点击信号的槽函数:获取输入框内容,构造 UDP 请求数据包,发送请求,清空输入框
- 处理服务器返回的响应:通过
readyRead 信号触发槽函数,读取响应数据显示到界面
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
#include <QNetworkDatagram>
#include <QHostAddress>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QUdpSocket *socket;
private slots:
void processResponse();
};
#endif
#include "widget.h"
#include "ui_widget.h"
QString SERVER_IP = "127.0.0.1";
quint16 SERVER_PORT = 8081;
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QUdpSocket(parent);
setWindowTitle("Udp 客户端");
connect(socket, &QUdpSocket::readyRead, this, &Widget::processResponse);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
QString context = ui->lineEdit->text();
QNetworkDatagram request(context.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
socket->writeDatagram(request);
ui->listWidget->addItem("客户端说:" + context);
ui->lineEdit->clear();
}
void Widget::processResponse()
{
const QNetworkDatagram& packet = socket->receiveDatagram();
QString s = QString::fromUtf8(packet.data());
ui->listWidget->addItem("服务器说" + s);
}
Qt UDP 总结
QNetworkDatagram 数据报类:发送和接收的数据报类,发送时构造传递 ip 和 port 指定目的地以及数据,接收时直接通过 socket 读取该类然后将 QByteArray 的二进制转换成 QString。
QUdpSocket 文件类:完成对消息的读写操作,并通过 QNetworkDatagram 完成消息传递。服务端需绑定 bind 服务器的 ip 和 port 信息,客户端通过 QNetworkDatagram 指定发送服务端信息。
&QUdpSocket::readyRead 消息准备就绪的信号:连接槽函数完成对 UDP 数据的处理。注意一定要先连接槽函数再绑定端口号。
.pro 中要添加 network。
2. TcpSocket
核心 API 概览,核心类是两个:QTcpServer 和 QTcpSocket,分别作用于服务端和客户端。
QTcpServer
| 名称 | 类型 | 说明 | 对标原生 API |
|---|
| listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听 | bind 和 listen |
| nextPendingConnection() | 方法 | 从系统中获取到一个已经建立好的 tcp 连接,返回一个 QTcpSocket | accept |
| newConnection | 信号 | 有新的客户端建立连接好之后触发 | 无 |
QTcpSocket
| 名称 | 类型 | 说明 | 对标原生 API |
|---|
| readAll() | 方法 | 读取当前接收缓冲区中的所有数据 | read |
| write(const QByteArray& ) | 方法 | 把数据写入 socket 中 | write |
| deleteLater | 方法 | 暂时把 socket 对象标记为无效 | 无 |
| peerAddress | 方法 | 获取对端客户端的 ip | 无 |
| peerPort | 方法 | 获取对端客户端 port | 无 |
| readyRead | 信号 | 有数据到达并准备就绪时触发 | 无 |
| disconnected | 信号 | 连接断开时触发 | 无 |
实操 Demo
Tcp 回显服务器
- 创建 Widget 项目(.pro 中添加 network)
- ui 文件中拖拽一个 ListWidget
- 添加
QTcpServer 类成员变量句柄
- 构造函数中修改窗口标题,创建
QTcpServer 实例,创建 newConnection 信号的信号槽 processConnection
- 在
processConnection 中:
- 通过
nextPendingConnection 获取客户端 socket
- 获取 ip 地址和端口,添加到 ListWidget
- 处理客户端发来请求:通过
readyRead 信号,获取请求报文,处理响应,写回客户端,记录日志
- 处理客户端断开连接:通过
disconnected 信号,显示下线信息,手动释放 clientSocket(使用 deleteLater)
- 绑定并监听端口号:确保先处理好连接逻辑再绑定监听,检查
listen 返回值
注意:TCP 是面向字节流的,存在粘包问题。更严谨的做法是约定应用层协议格式。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle("服务器");
server = new QTcpServer(parent);
connect(server, &QTcpServer::newConnection, this, &Widget::processConnection);
bool ret = server->listen(QHostAddress::Any, 8080);
if (!ret) {
QMessageBox::critical(this, "服务器启动失败", server->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
QString Widget::process(QString& request)
{
return request;
}
void Widget::processConnection()
{
QTcpSocket *clientSocket = server->nextPendingConnection();
QHostAddress add = clientSocket->peerAddress();
quint16 port = clientSocket->peerPort();
ui->listWidget->addItem("[" + add.toString() + ":" + QString::number(port) + "]:" + "客户端已上线");
connect(clientSocket, &QTcpSocket::readyRead, this, [=]() {
QString request = clientSocket->readAll();
const QString& response = process(request);
clientSocket->write(response.toUtf8());
QString log = "[" + add.toString() + ":" + QString::number(port) + "] req:" + request + ",res:" + response;
ui->listWidget->addItem(log);
});
connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {
QString log = "[" + add.toString() + ":" + QString::number(port) + "]已下线";
qDebug() << log;
ui->listWidget->addItem(log);
clientSocket->deleteLater();
});
}
Tcp 客户端的实现
- 创建新的 Widget 项目
- 创建
QTcpSocket 对象 (socket)
- 构造函数中设置窗口,创建 socket 实例,和服务器建立连接
connectToHost
- 通过
readyRead 信号处理响应
- 等待连接建立的结果,确定是否连接成功
waitForConnected
- 给按钮添加点击的槽函数:获取输入框内容,发送内容给服务器,显示消息,清空输入框
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
socket = new QTcpSocket(parent);
socket->connectToHost("127.0.0.1", 8080);
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QString response = socket->readAll();
ui->listWidget->addItem("服务端说:" + response);
});
if (!socket->waitForConnected()) {
QMessageBox::critical(this, "client 连接服务器失败", socket->errorString());
exit(0);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
QString request = ui->lineEdit->text();
socket->write(request.toUtf8());
ui->listWidget->addItem("客户端说:" + request);
ui->lineEdit->clear();
}
Qt Tcp 总结
- 服务器中
QTcpServer::newConnection 处理新链接的信号,注意需要连接从而完成新链接新用户的消息处理和断开连接处理。
nextPendingConnection() 从就绪的连接中获取一个,从而拿到一个客户端(QTcpSocket *)。
QTcpSocket 常用的函数:peerAddress(获取地址)、peerPort(端口)、readAll(获取 socket 中的数据)、write(通过 socket 发送消息数据)、connectToHost(客户端中使用)。
QTcpSocket::readyRead,同前面客户端消息就绪的信号。
- 注意 QT 信号槽中使用 lambda 时不要引用(&)获取而是值(=)获取。
QTcpSocket::disconnected 用户断开时会触发的信号。
3. HTTP Client
Qt 中也是提供了 Http 的客户端,Http 协议本质上也就是基于 TCP 协议实现的。
核心 API
关键类主要是三个:QNetworkAccessManager, QNetworkRequest, QNetworkReply。
QNetworkAccessManager
| 方法 | 说明 |
|---|
| get(const QNetworkRequest& ) | 发起一个 HTTP GET 请求,返回 QNetworkReply 对象 |
| post(const QNetworkRequest& , const QByteArray& ) | 发起一个 HTTP POST 请求,返回 QNetworkReply 对象 |
QNetworkRequest
| 方法 | 说明 |
|---|
| QNetworkRequest(const QUrl& ) | 通过 URL 构造一个 HTTP 请求 |
| setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) | 设置请求头 |
常用 Header 取值:ContentTypeHeader, ContentLengthHeader, CookieHeader, UserAgentHeader。
QNetworkReply
表示一个 HTTP 响应。这个类同时也是 QIODevice 的子类。
| 方法 | 说明 |
|---|
| error() | 获取出错状态 |
| errorString() | 获取出错原因的文本 |
| readAll() | 读取响应 body |
| header(QNetworkRequest::KnownHeaders header) | 读取响应指定 header 的值 |
实操 Demo Http 客户端
.pro 中添加 network 标识
- 添加
QNetworkAccessManger 成员变量(client)
- 构造函数中实例化成员变量,指定父元素
- 给按钮添加点击的槽函数:
- 获取输入框中的 url
- 通过 client 调用 get 方法并传递 url
- 构造一个 HTTP 请求对象 request,会返回一个 QNetworkReply 响应对象
- 注意 get 只负责发送请求,并不会阻塞等待,通过 response 内部的 finished 信号处理
- 连接 finished 信号,判断 error,读取 response 的 html 数据,设置到 PlainTextEdit 中,最后调用 response 的 deleteLater
注意最后这里访问的是 http 的而非 https。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
client = new QNetworkAccessManager(parent);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
QUrl url = ui->lineEdit->text();
QNetworkRequest request(url);
QNetworkReply *response = client->get(request);
connect(response, &QNetworkReply::finished, this, [=]() {
if (response->error() == QNetworkReply::NoError) {
QString req = response->readAll();
ui->plainTextEdit->setPlainText(req);
} else {
ui->plainTextEdit->setPlainText(response->errorString());
}
response->deleteLater();
});
}
Qt Http 总结
QNetworkRequest 构造一个 http 请求,初始化填入 url 代表路径。
- url 需要的类型是 QUrl,QString 能直接转换。
QNetworkAccessManager 中的 get/post 函数进行发送请求和并且接收响应 QNetworkReply,但需要注意的是响应处不是阻塞的。
- 所以需要设置一个信号槽,信号是
QNetworkReply::finished 当数据到达时就会触发。
- 当 QNetworkReply 的
error 函数和 QNetworkReply::NoError 相等就代表没有问题此时就能在继续处理 http 响应了。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online