【C++/Qt】Qt 实现 HTTP 测试工具:从请求构思到 GET/POST 实现

本文基于 C++/Qt 实现一个简单的 HTTP 测试工具,支持 GET、POST 请求,请求头设置、请求体输入、响应结果显示和日志记录。重点不是单纯堆代码,而是从 HTTP 的基本概念出发,梳理一个 HTTP 测试模块应该如何设计,以及 Qt 中如何借助 QNetworkAccessManagerQNetworkRequestQNetworkReply 完成请求发送和响应处理。本文结合的 FormHttpTester 类中,已经包含 HTTP 请求管理器、当前请求对象、日志缓存和请求状态标记等成员设计。

一、HTTP 是什么?为什么会有 GET 和 POST?

HTTP 可以理解为一种"客户端和服务器之间说话的规则"。

平时打开网页、登录账号、提交表单、请求接口,本质上都是客户端向服务器发送 HTTP 请求,然后服务器返回 HTTP 响应。

一次 HTTP 通信大概可以理解成这样:

复制代码
客户端:我要访问这个地址,并且带上一些请求信息
服务器:我收到请求了,返回状态码、响应头和响应内容

比如访问一个接口时,请求里面通常会包含:

复制代码
请求地址:URL,例如 https://example.com/api/user
请求方法:GET 或 POST
请求头:Content-Type、Authorization 等
请求体:POST 时常见,例如 JSON 数据

服务器返回时,一般会包含:

复制代码
状态码:200、404、500 等
响应头:服务器返回的附加信息
响应体:真正的数据内容,例如 JSON、HTML、普通文本

其中 GET 和 POST 是最常见的两种请求方式。

GET 更像是"向服务器要数据"。比如查询用户信息、获取文章列表、打开某个网页。它通常把参数放在 URL 后面,例如:

复制代码
https://example.com/api/user?id=1001

POST 更像是"向服务器提交数据"。比如登录、注册、提交表单、发送 JSON 数据。它通常把数据放在请求体 Body 里面,例如:

复制代码
{
    "username": "test",
    "password": "123456"
}

所以简单理解就是:

复制代码
GET:主要用于获取数据,一般不写请求体
POST:主要用于提交数据,通常需要请求体

这也是为什么在 HTTP 测试工具界面中,选择 GET 时可以禁用请求体输入框,而选择 POST 时再开启请求体输入框。这个设计不是随便做的,而是符合 GET/POST 本身的使用习惯。

1. 请求头 Header 是什么?

请求头就是 HTTP 请求里的"说明信息",它不一定是要提交的数据本身,而是告诉服务器:

cpp 复制代码
这次请求是什么格式?
客户端是谁?
有没有登录凭证?
希望服务器返回什么类型的数据?

比如常见请求头:

cpp 复制代码
Content-Type: application/json
Authorization: Bearer xxxxx
User-Agent: QtHttpTester
Accept: application/json

简单解释一下:

cpp 复制代码
Content-Type:告诉服务器,请求体里的数据是什么格式
Authorization:告诉服务器,当前用户的身份凭证
User-Agent:告诉服务器,当前请求来自什么客户端
Accept:告诉服务器,客户端希望接收什么格式的数据

比如:

cpp 复制代码
Content-Type: application/json

意思就是:我这次提交的数据是 JSON 格式,你服务器解析的时候要按 JSON 来解析。所以请求头更像是"请求的附加说明"。

2. 请求体 Body 是什么?

请求体就是这次请求真正要提交给服务器的数据。

比如登录时,可能要提交账号密码:

cpp 复制代码
{
    "username": "test",
    "password": "123456"
}

这些真正的数据就放在请求体 Body 里面。

所以可以简单理解:

cpp 复制代码
请求头 Header:告诉服务器"这份数据应该怎么看"
请求体 Body:真正要交给服务器的数据内容

二、实现 HTTP 测试工具时,应该先构思哪些功能?

写 HTTP 模块之前,先不要急着写代码,而是先想:一个用户要测试 HTTP 接口时,需要输入什么?又希望看到什么?

从使用流程上看,应该是这样:

复制代码
输入 URL
选择 GET 或 POST
填写请求头
如果是 POST,再填写请求体
点击发送请求
查看响应状态、响应头、响应体
必要时复制响应或保存日志

所以这个模块的界面和功能可以围绕这几个部分设计:

复制代码
1. 请求区:URL、GET/POST、请求头、请求体
2. 操作区:发送、清空、复制响应
3. 响应区:响应头、响应体、状态显示
4. 日志区:记录请求时间、请求结果、错误信息

在 Qt 代码中,HTTP 模块主要依靠三个类完成:

复制代码
QNetworkAccessManager:负责真正发送 HTTP 请求
QNetworkRequest:负责描述一次请求,包括 URL 和请求头
QNetworkReply:负责接收服务器返回的响应

因此类中需要准备几个核心成员:

cpp 复制代码
private:
    Ui::FormHttpTester *ui;

    // HTTP 请求管理器,负责发送 GET / POST 请求
    QNetworkAccessManager *m_manager = nullptr;

    // 当前正在执行的请求,用于读取响应或中止请求
    QNetworkReply *m_currentReply = nullptr;

    // 日志缓存,用于记录每一次请求过程
    QStringList m_logEntries;

    // 标记当前是否正在请求,避免用户重复点击发送
    bool m_requestRunning = false;

这种设计的好处是比较清晰:
m_manager 负责发送,m_currentReply 负责接收,m_logEntries 负责记录,m_requestRunning 负责控制请求状态。

三、界面初始化:让 GET/POST 的使用逻辑更清楚

HTTP 测试工具启动后,需要先初始化界面。比如设置 GET/POST 选项,设置输入框提示,默认添加 Content-Type: application/json,并且让响应区域只读。

cpp 复制代码
void FormHttpTester::initUi()
{
    // 初始化请求方法,只提供 GET 和 POST
    ui->comboBox_HttpMethod->clear();
    ui->comboBox_HttpMethod->addItem("GET");
    ui->comboBox_HttpMethod->addItem("POST");

    // 设置 URL 输入提示,提醒用户输入完整地址
    ui->lineEdit_HttpUrl->setPlaceholderText("例如:https://example.com/api/test");

    // 请求头区域,每一行表示一个请求头
    ui->textEdit_HttpHeaders->setPlaceholderText(
        "每行一个请求头,例如:\nContent-Type: application/json"
    );

    // 请求体主要用于 POST 请求
    ui->textEdit_HttpBody->setPlaceholderText(
        "POST请求体,例如:\n{\"name\":\"test\"}"
    );

    // 默认使用 JSON 请求头,方便测试常见接口
    ui->textEdit_HttpHeaders->setPlainText("Content-Type: application/json");

    // GET 请求通常不需要请求体,所以默认禁用
    ui->textEdit_HttpBody->setEnabled(false);

    // 响应头和响应体只用于显示,不允许手动编辑
    ui->plainTextEdit_HttpResponseHeaders->setReadOnly(true);
    ui->plainTextEdit_HttpResponseBody->setReadOnly(true);

    // 初始状态
    ui->label_HttpStatus->setText("状态:未发送请求");
}

这里最值得注意的是这一句:

cpp 复制代码
ui->textEdit_HttpBody->setEnabled(false);

因为默认是 GET 请求,而 GET 通常不需要请求体,所以先把请求体输入框禁用掉。

当用户切换请求方式时,再根据当前选择动态开启或关闭请求体:

cpp 复制代码
void FormHttpTester::onMethodChanged(int index)
{
    // 获取当前选择的请求方式
    const QString method = ui->comboBox_HttpMethod->itemText(index);

    // POST 需要请求体,GET 通常不需要请求体
    ui->textEdit_HttpBody->setEnabled(method == "POST" && !m_requestRunning);
}

这个地方体现的是一个很重要的界面设计思路:

不是把所有控件都一直开放,而是根据当前请求方式,让界面状态跟着业务逻辑变化。

四、发送 HTTP 请求:URL 校验、请求头解析、GET/POST 分发

点击"发送"按钮后,程序不能直接发请求,而是要先检查用户输入是否合理。

比如 URL 是否为空,是否包含 http://https://,是否有主机名。如果这些都不检查,用户随便输入一个字符串,后面请求失败时就很难判断问题出在哪里。

核心发送流程如下:

cpp 复制代码
void FormHttpTester::onSendRequest()
{
    // 如果当前已有请求正在执行,直接返回,避免重复发送
    if (m_requestRunning) {
        return;
    }

    // 读取用户输入的 URL,并去掉首尾空格
    const QString urlText = ui->lineEdit_HttpUrl->text().trimmed();

    // URL 不能为空
    if (urlText.isEmpty()) {
        QMessageBox::warning(this, "提示", "请输入请求地址。");
        return;
    }

    // 将字符串转换为 QUrl,方便 Qt 判断 URL 格式
    const QUrl url(urlText);

    // 判断 URL 是否有效,必须有协议和主机名
    if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) {
        QMessageBox::warning(this, "提示",
                             "请求地址格式无效,请输入完整的 http:// 或 https:// 地址。");
        return;
    }

    // 当前工具只支持 HTTP 和 HTTPS
    if (url.scheme() != "http" && url.scheme() != "https") {
        QMessageBox::warning(this, "提示", "当前仅支持 http:// 或 https:// 请求。");
        return;
    }

    // 构造请求对象
    QNetworkRequest request = buildRequest(url);

    // 获取当前请求方式
    const QString method = ui->comboBox_HttpMethod->currentText();

    // 清空上一次响应结果
    ui->plainTextEdit_HttpResponseHeaders->clear();
    ui->plainTextEdit_HttpResponseBody->clear();
    ui->label_HttpStatus->setText("状态:请求发送中...");

    // 设置请求忙碌状态
    setRequestBusy(true);

    // 根据 GET / POST 调用不同函数
    if (method == "GET") {
        m_currentReply = m_manager->get(request);
    } else {
        // POST 请求需要带上请求体数据
        const QByteArray bodyData = ui->textEdit_HttpBody->toPlainText().toUtf8();
        m_currentReply = m_manager->post(request, bodyData);
    }

    // 请求完成后,进入响应处理函数
    connect(m_currentReply, &QNetworkReply::finished,
            this, &FormHttpTester::onReplyFinished);

    // 请求出错时,触发错误处理函数
    connect(m_currentReply, &QNetworkReply::errorOccurred,
            this, &FormHttpTester::onReplyError);
}

这一段里面最核心的就是:

cpp 复制代码
m_currentReply = m_manager->get(request);
m_currentReply = m_manager->post(request, bodyData);

GET 请求只需要一个 QNetworkRequest,因为它通常不带请求体。

POST 请求除了 QNetworkRequest,还要传入请求体数据 bodyData

请求头的处理单独封装成了 applyHeaders(),这样代码不会全部挤在 onSendRequest() 里面:

cpp 复制代码
void FormHttpTester::applyHeaders(QNetworkRequest &request) const
{
    // 将请求头输入框内容按行拆分
    const QStringList lines = ui->textEdit_HttpHeaders->toPlainText().split('\n');

    for (const QString &line : lines) {
        // 去掉每行前后空格
        const QString trimmedLine = line.trimmed();

        // 空行直接跳过
        if (trimmedLine.isEmpty()) {
            continue;
        }

        // 请求头格式应该是 Key: Value,所以先找到冒号
        const int colonIndex = trimmedLine.indexOf(':');

        // 冒号不存在,或者冒号在最前面,说明格式不正确
        if (colonIndex <= 0) {
            continue;
        }

        // 冒号前面是请求头名称
        const QString key = trimmedLine.left(colonIndex).trimmed();

        // 冒号后面是请求头内容
        const QString value = trimmedLine.mid(colonIndex + 1).trimmed();

        // 设置原始请求头
        if (!key.isEmpty()) {
            request.setRawHeader(key.toUtf8(), value.toUtf8());
        }
    }
}

例如界面里输入:

cpp 复制代码
Content-Type: application/json
Authorization: Bearer token

程序就会把它们转换成真正的 HTTP 请求头。

所以发送请求这一块的思路可以总结为:

cpp 复制代码
检查 URL
构造 QNetworkRequest
解析请求头
判断 GET 或 POST
调用 QNetworkAccessManager 发送请求
等待 QNetworkReply 返回

五、响应处理:显示状态码、响应头、响应体,并记录日志

HTTP 请求是异步的。

也就是说,调用 get()post() 后,并不是马上拿到结果,而是请求先发出去,等服务器返回后,Qt 会触发 finished 信号。

因此真正读取响应内容的地方在 onReplyFinished() 中。代码中会读取错误信息、HTTP 状态码、响应头和响应体,然后显示到界面上。当前实现中,请求完成后会读取 HttpStatusCodeAttributeHttpReasonPhraseAttributerawHeaderList()readAll(),并根据是否出错更新状态标签和日志。

cpp 复制代码
void FormHttpTester::onReplyFinished()
{
    // 如果当前没有响应对象,直接恢复状态
    if (!m_currentReply) {
        setRequestBusy(false);
        return;
    }

    // 获取网络错误码和错误信息
    const QNetworkReply::NetworkError errorCode = m_currentReply->error();
    const QString errorString = m_currentReply->errorString();

    // 获取 HTTP 状态码,例如 200、404、500
    const int statusCode =
        m_currentReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    // 获取状态描述,例如 OK、Not Found
    const QString reason =
        m_currentReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();

    // 读取响应头
    QString headerText;
    const QList<QByteArray> headerList = m_currentReply->rawHeaderList();

    for (const QByteArray &headerName : headerList) {
        headerText += QString::fromUtf8(headerName);
        headerText += ": ";
        headerText += QString::fromUtf8(m_currentReply->rawHeader(headerName));
        headerText += "\n";
    }

    // 读取响应体
    const QByteArray responseData = m_currentReply->readAll();
    const QString responseText = QString::fromUtf8(responseData);

    // 显示响应头和响应体
    ui->plainTextEdit_HttpResponseHeaders->setPlainText(headerText);
    ui->plainTextEdit_HttpResponseBody->setPlainText(responseText);

    // 根据请求是否成功,更新状态显示
    if (errorCode == QNetworkReply::NoError) {
        ui->label_HttpStatus->setText(QString("状态:%1 %2,响应大小:%3 字节")
                                          .arg(statusCode)
                                          .arg(reason)
                                          .arg(responseData.size()));
    } else {
        ui->label_HttpStatus->setText(QString("状态:请求失败,错误:%1").arg(errorString));
    }

    // 释放当前响应对象,避免内存泄漏
    m_currentReply->deleteLater();
    m_currentReply = nullptr;

    // 请求结束,恢复界面按钮状态
    setRequestBusy(false);
}

这里有几个点要注意。

第一,响应体通过:

cpp 复制代码
m_currentReply->readAll();

读取。它读出来的是 QByteArray,也就是字节数据。为了显示到界面上,可以转换成字符串:

cpp 复制代码
const QString responseText = QString::fromUtf8(responseData);

第二,请求完成后一定要释放 QNetworkReply

cpp 复制代码
m_currentReply->deleteLater();
m_currentReply = nullptr;

因为每次请求都会生成一个新的 QNetworkReply 对象。如果不释放,请求次数多了就可能造成资源浪费。

第三,请求过程中要控制按钮状态,避免重复发送:

cpp 复制代码
void FormHttpTester::setRequestBusy(bool busy)
{
    // 记录当前是否正在请求
    m_requestRunning = busy;

    // 请求过程中禁用发送按钮,避免重复点击
    ui->pushButton_HttpSend->setEnabled(!busy);

    // 请求过程中不允许修改请求方式、URL 和请求头
    ui->comboBox_HttpMethod->setEnabled(!busy);
    ui->lineEdit_HttpUrl->setEnabled(!busy);
    ui->textEdit_HttpHeaders->setEnabled(!busy);

    // 只有 POST 并且当前不忙碌时,才允许编辑请求体
    const bool isPost = ui->comboBox_HttpMethod->currentText() == "POST";
    ui->textEdit_HttpBody->setEnabled(!busy && isPost);
}

这个函数看起来只是控制控件是否可用,但实际很重要。因为 HTTP 请求是异步的,如果请求还没结束,用户又连续点击发送,就可能导致多个响应互相覆盖,界面状态也容易混乱。

总结

实现 Qt HTTP 测试工具,核心不是手写 HTTP 协议,而是理解"请求---响应"这个过程。

整体思路可以概括为:

cpp 复制代码
HTTP 是客户端和服务器之间通信的规则
GET 主要用于获取数据,通常不需要请求体
POST 主要用于提交数据,通常需要请求体
Qt 中使用 QNetworkAccessManager 发送请求
使用 QNetworkRequest 描述 URL 和请求头
使用 QNetworkReply 接收状态码、响应头和响应体

从代码结构上看,一个比较清晰的 HTTP 模块应该把功能拆开:

cpp 复制代码
initUi():初始化界面
onSendRequest():发送请求
applyHeaders():解析请求头
onReplyFinished():处理响应
setRequestBusy():控制请求过程中的界面状态

这样实现出来的 HTTP 测试工具虽然功能不复杂,但已经具备了一个基础接口调试模块该有的完整流程:用户输入请求信息,程序发送 GET/POST 请求,服务器返回结果,界面显示响应内容,并记录请求日志。对于网络调试助手项目来说,这个模块可以作为 TCP/UDP 之外的一个应用层协议测试功能,让整个工具更加完整。

0voice · GitHub

相关推荐
jf加菲猫6 小时前
第16章 容器类
开发语言·c++·qt·ui
人道领域6 小时前
从零实现一个轻量级 RPC 框架:通信协议与动态代理的核心原理
开发语言·网络·qt
fish_xk6 小时前
二叉搜索树
c++
灰子学技术6 小时前
Envoy HTTP 协议实现技术文档
网络·网络协议·http
熬夜敲代码的猫6 小时前
C++:让你玩转多态
c++·多态
qeen876 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
lynnlovemin6 小时前
C++高精度加减乘除算法详解
开发语言·c++·算法·高精度
minji...6 小时前
Linux 网络套接字编程(七)TCP服务端和客户端的实现——网络版本计算器
linux·运维·服务器·网络·c++·tcp/ip·udp
rrr26 小时前
【PyQt5】| 多线程设计模式
开发语言·qt·设计模式