• 【QT HTTP】使用QtNetwork模块制作基于HTTP请求的C/S架构


    请添加图片描述

    • 🙋‍♂️ 作者:海码007
    • 📜 专栏:C++专栏
    • 💥 标题:【QT HTTP】使用QtNetwork模块制作基于HTTP请求的C/S架构
    • ❣️ 寄语:书到用时方恨少,事非经过不知难。
    • 🎈 最后:文章作者技术和水平有限,如果文中出现错误,希望大家能指正!

    0 引言

    • 最近项目涉及到网络HTTP相关内容,需要处理客户端发送的POST、GET等请求。所以本文使用QT的QtNetwork模块来实现一个简单的发送POST、GET请求的客户端,然后响应POST、GET请求的服务器。
    • 本文涉及到一些HTTP的基本知识,可以参考博主之前的文章:计算机网络HTTP协议

    1 HTTP基本知识

    1.1 请求类型

    HTTP(Hypertext Transfer Protocol)协议 定义了多种请求方法,也称为HTTP请求类型或HTTP动词。这些请求方法表示客户端希望对特定资源执行的操作。以下是常见的HTTP请求类型、其功能和应用场景:

    1. GET:

      • 功能: 用于从服务器获取指定资源的信息。请求的参数通常附在URL后面,通过查询字符串传递。
      • 应用场景: 用于查看网页、下载文件、获取数据等。是幂等的,不应该对服务器产生影响。(我们输入一个网址,其实就是从服务器获得一个HTML文件,然后浏览器内核再根据其将内容绘制出来)
    2. POST:

      • 功能: 用于向服务器提交数据,通常用于表单提交。请求的参数通常包含在请求体中。
      • 应用场景: 用于创建新资源、提交表单数据、上传文件等。可能对服务器产生影响。不是幂等的,多次相同的POST请求可能产生不同的结果。
    3. PUT:

      • 功能: 用于向服务器上传新资源,或者更新已存在的资源。请求的参数通常包含在请求体中。
      • 应用场景: 用于创建或更新资源。是幂等的,多次相同的PUT请求应该产生相同的结果。
    4. DELETE:

      • 功能: 用于请求服务器删除指定的资源
      • 应用场景: 用于删除指定资源。是幂等的,多次相同的DELETE请求应该产生相同的结果。
    5. PATCH:

      • 功能: 用于对资源进行部分更新。请求的参数通常包含在请求体中,表示对资源的局部修改。
      • 应用场景: 用于对资源进行局部更新,而不是替换整个资源。
    6. HEAD:

      • 功能: 类似于GET请求,但服务器只返回响应头,不返回实体主体。常用于检查资源的元信息,如是否存在、是否已经修改等。
      • 应用场景: 用于获取资源的头部信息,而不需要获取整个资源的内容。
    7. OPTIONS:

      • 功能: 用于获取目标资源支持的通信选项。客户端可以通过这个方法了解服务器支持的方法。
      • 应用场景: 用于确定服务器支持的方法,以及支持的头信息等。
    8. TRACE:

      • 功能: 用于在目标服务器上执行一个消息环回测试,客户端发送的请求会在最终的服务器上返回,用于诊断和调试。
      • 应用场景: 主要用于网络诊断,通常不会在实际应用中直接使用。

    选择适当的HTTP请求类型取决于具体的操作和业务需求。每种请求类型都有其独特的功能和应用场景,使其适用于不同的情境。

    1.2 HTTP请求报文格式

    HTTP请求报文是客户端发送给服务器的文本信息,包含请求的各种参数和头信息。它的基本格式如下:

      
    
    
    
    
    • 1
    • 2
    • 3
    • 4

    其中,各部分的含义如下:

    • :HTTP请求方法,例如GET、POST、PUT等。
    • :请求的资源标识符,通常是一个URL。
    • :使用的HTTP协议版本,例如HTTP/1.1。
    • :包含多行的头部信息,每行都包含一个头字段和对应的值。
    • :可选的请求体,用于包含请求时需要发送的数据,例如POST请求中的表单数据。

    以下是一个具体的例子:

    GET /index.html HTTP/1.1
    Host: www.example.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这个例子中:

    • 请求方法是GET。
    • 请求的资源标识符是/index.html
    • 使用的HTTP协议版本是HTTP/1.1。
    • 请求头部包含了HostUser-AgentAccept等字段,每个字段都以: 的形式呈现。
    • 由于GET请求通常不包含请求体,因此没有部分。

    对于包含请求体的请求,例如POST请求,请求体会紧随请求头部,并用一个空行分隔。例如:

    POST /submit-form HTTP/1.1
    Host: www.example.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 23
    
    username=johndoe&password=123
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这个例子中,请求体包含了表单数据username=johndoe&password=123,并通过Content-Type头字段指定了数据的格式。

    1.3 HTTP响应报文格式

    HTTP响应报文是服务器返回给客户端的文本信息,包含了服务器对客户端请求的响应。其基本格式如下:

      
    
    
    
    
    • 1
    • 2
    • 3
    • 4

    其中,各部分的含义如下:

    • :使用的HTTP协议版本,例如HTTP/1.1。
    • :一个三位数的状态码,表示服务器对请求的处理结果。
    • :状态码的文本描述,描述了状态码的原因。
    • :包含多行的头部信息,每行都包含一个头字段和对应的值。
    • :可选的响应体,用于包含服务器返回给客户端的数据。

    以下是一个具体的例子:

    HTTP/1.1 200 OK
    Date: Mon, 15 Nov 2023 12:00:00 GMT
    Server: Apache
    Content-Type: text/html
    Content-Length: 1234
    Connection: keep-alive
    
    
      
        Hello, World!
      
      
        

    Welcome to my website!

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这个例子中:

    • 使用的HTTP协议版本是HTTP/1.1。
    • 状态码是200,表示请求成功。
    • 原因短语是"OK",为状态码的文本描述。
    • 响应头部包含了DateServerContent-Type等字段。
    • 由于这是一个简单的HTML响应,响应体包含了一个HTML文档。

    对于包含响应体的响应,例如HTML页面、JSON数据等,响应体会紧随响应头部,并用一个空行分隔。响应体的格式和内容取决于服务器的实际响应。

    1.4 拓展:GET vs POST 请求方法

    GET和POST请求在HTTP中的请求报文和响应报文中有一些区别,这主要涉及到数据的传递方式和一些特定的语义约定。


    GET请求

    请求报文:
    1. 参数传递: GET请求的参数通常附在URL的查询字符串中,通过?&符号进行连接,例如:http://example.com/resource?param1=value1¶m2=value2
    2. 请求体: GET请求通常没有请求体,因为它用于请求资源,而不是向服务器提交数据。
    响应报文
    1. 响应体: GET请求的响应体包含了服务器返回的资源数据。

    POST请求

    请求报文
    1. 参数传递: POST请求的参数通常包含在请求体中,而不是在URL中,特别是用于提交表单数据或上传文件等场景。
    2. 请求体: POST请求的请求体包含了客户端提交给服务器的数据。

    响应报文
    1. 响应体: POST请求的响应体包含了服务器对提交的数据的处理结果。

    其他注意事项

    1. 安全性: POST请求的数据包含在请求体中,相对于GET请求,POST请求具有更好的安全性,因为它不会在URL中明文传递敏感信息。
    2. 幂等性: GET请求是幂等的,多次相同的GET请求应该产生相同的结果。POST请求是非幂等的,多次相同的POST请求可能会产生不同的结果。

    示例:

    GET请求示例

    请求报文:

    GET /resource?param1=value1¶m2=value2 HTTP/1.1
    Host: example.com
    
    • 1
    • 2

    响应报文:

    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 1234
    
    
    
      
        GET Response
      
      
        

    This is the response to a GET request.

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    POST请求示例

    请求报文:

    POST /submit-form HTTP/1.1
    Host: example.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 27
    
    param1=value1¶m2=value2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    响应报文:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Content-Length: 45
    
    {"status": "success", "message": "POST response"}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总的来说,GET和POST请求的区别主要在于参数传递的方式、请求体的内容和请求的语义。GET适用于获取资源,而POST适用于向服务器提交数据。

    2 实战

    2.1 QtNetwork模块介绍

    QtNetwork模块是Qt中用于网络编程的模块,提供了一系列用于处理网络通信的类和工具。以下是QtNetwork模块的一些主要功能:

    1. TCP和UDP通信: 提供QTcpSocketQUdpSocket等类,用于实现TCP和UDP协议的通信。这些类使得在Qt应用程序中创建和管理网络连接变得相对简单。

    2. HTTP客户端和服务器: 提供QNetworkAccessManager类,用于实现HTTP协议的客户端功能。它支持GET、POST等HTTP请求方法,并允许异步地发送和接收HTTP请求。

    3. 网络请求和响应处理: 提供QNetworkRequestQNetworkReply等类,用于构建和处理网络请求。这些类提供了丰富的功能,包括请求头的设置、数据的传输和响应的处理等。

    4. FTP客户端: 提供QFtp类,用于实现FTP协议的客户端功能。它允许在Qt应用程序中进行文件传输操作。

    5. 网络代理: 支持网络代理设置,可以通过QNetworkProxy类配置网络代理,以便在需要时通过代理服务器进行网络通信。

    6. 网络协议支持: QtNetwork模块支持各种网络协议,包括IPv4和IPv6,SSL/TLS等。这使得Qt应用程序能够适应多种网络环境和安全需求。

    7. 网络状态监控: 提供QNetworkConfigurationQNetworkConfigurationManager类,用于监控和管理网络配置,以便在应用程序中适应不同的网络状态。

    8. 网络缓存: 提供QNetworkDiskCache等类,用于实现网络缓存,以提高应用程序的性能并减少对网络资源的依赖。

    这些功能使QtNetwork成为一个强大的网络编程工具,适用于开发涉及网络通信的各种应用,从简单的客户端到复杂的服务器应用。

    2.2 编程实现HTTP客户端

    根据上述描述,可以知道,使用 QTcpSocketQUdpSocketQNetworkAccessManagerQNetworkRequestQNetworkReply等类可以实现简单的HTTP客户端。

    接下来是代码:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        // 创建网络访问管理器
        QNetworkAccessManager manager;
    
        // 创建HTTP请求
        QNetworkRequest getRequest(QUrl("http://example.com"));
    
        // 发送GET请求
        QNetworkReply *getReply = manager.get(getRequest);
    
        // 处理GET请求完成的信号
        QObject::connect(getReply, &QNetworkReply::finished, [&]() {
            if (getReply->error() == QNetworkReply::NoError) {
                qDebug() << "GET Response:" << getReply->readAll();
            } else {
                qDebug() << "GET Error:" << getReply->errorString();
            }
    
            getReply->deleteLater();
        });
    
        // 进入应用程序事件循环
        return a.exec();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在使用 Qt 进行网络请求时,尤其是在进行异步的网络操作时,需要进入应用程序的事件循环。这是因为 Qt 的事件循环负责处理事件,而网络请求的完成(比如接收到服务器的响应)通常是通过 Qt 的信号-槽机制来处理的。

    让我们来详细解释一下:

    1. 异步操作: Qt 的网络操作通常是异步的,即在发起网络请求后,程序会继续执行后续的代码而不等待请求完成。这是为了确保应用程序的界面和其他部分能够保持响应性,不被阻塞。

    2. 信号-槽机制: 当网络请求完成时,QNetworkReply 会发出 finished 信号。你在代码中使用 QObject::connect 来连接这个信号到一个槽函数,以便在请求完成时执行一些操作。

    3. 事件循环: 为了让信号-槽机制正常工作,需要进入应用程序的事件循环。调用 QCoreApplication::exec() 或者 QEventLoop::exec() 启动事件循环,使得 Qt 可以不断地检查并处理事件队列。

    在代码中,调用 return a.exec(); 启动了事件循环。这样,当 GET 请求完成并发出 finished 信号时,相关的槽函数将会被执行。如果没有进入事件循环,这个槽函数将不会被触发,因为事件循环负责调度信号的处理。

    简而言之,进入应用程序的事件循环是确保异步操作和信号-槽机制正常工作的关键步骤。如果你的应用程序没有事件循环,它将无法及时响应和处理异步操作的完成事件。

    2.3 编程实现HTTP服务器

    #ifndef MYHTTPSERVER_H
    #define MYHTTPSERVER_H
    
    #include 
    #include 
    #include 
    #include 
    
    class MyHTTPServer : public QTcpServer
    {
        Q_OBJECT
    
    public:
        MyHTTPServer(QObject *parent = nullptr) : QTcpServer(parent) {}
    
    protected:
        //--------------------------------------
        // 说明:这是 QTcpServer 类的虚函数,当有新的连接到达时,会被调用。
        // 日期:2023-11-15
        // 作者:何浩文
        //--------------------------------------
        void incomingConnection(qintptr socketDescriptor) override
        {
            QTcpSocket *socket = new QTcpSocket(this);
            socket->setSocketDescriptor(socketDescriptor);
    
            // 读取客户端请求
            connect(socket, &QTcpSocket::readyRead, [&]() {
                QByteArray requestData = socket->readAll();
                processRequest(requestData, socket);
    
                // 关闭连接
                socket->disconnectFromHost();
            });
    
            // 处理连接断开
            connect(socket, &QTcpSocket::disconnected, [&]() {
                socket->deleteLater();
            });
        }
    
    private:
        //--------------------------------------
        // 说明:这个函数用于解析 HTTP 请求,分析请求的方法和路径,并调用相应的处理函数。
        // 日期:2023-11-15
        // 作者:海码007
        //--------------------------------------
        void processRequest(const QByteArray &requestData, QTcpSocket *socket)
        {
            // 解析请求
            QString requestString = QString::fromUtf8(requestData);
            QStringList requestLines = requestString.split("\r\n");
    
            // 解析第一行,获取请求方法和路径
            QString firstLine = requestLines.first();
            QStringList parts = firstLine.split(" ");
            QString method = parts.value(0);
            QString path = parts.value(1);
    
            // 处理 GET 请求
            if (method == "GET")
            {
                handleGetRequest(path, socket);
            }
            // 处理 POST 请求
            else if (method == "POST")
            {
                handlePostRequest(path, requestData, socket);
            }
        }
    
        //--------------------------------------
        // 说明:处理 HTTP GET 请求的具体逻辑。
        // 日期:2023-11-15
        // 作者:海码007
        //--------------------------------------
        void handleGetRequest(const QString &path, QTcpSocket *socket)
        {
            QTextStream responseStream(socket);
            responseStream.setAutoDetectUnicode(true);
    
            // 构造HTTP响应
            responseStream << "HTTP/1.1 200 OK\r\n"
                           << "Content-Type: text/html\r\n"
                           << "Connection: close\r\n"
                           << "\r\n"
                           << "

    Hello, World! (GET)

    "
    ; // 刷新并等待数据发送完毕 socket->flush(); socket->waitForBytesWritten(); } //-------------------------------------- // 说明:处理 HTTP POST 请求的具体逻辑。 // 日期:2023-11-15 // 作者:海码007 //-------------------------------------- void handlePostRequest(const QString &path, const QByteArray &requestData, QTcpSocket *socket) { // 解析 POST 数据 QUrlQuery postData(requestData); QString value = postData.queryItemValue("key"); QTextStream responseStream(socket); responseStream.setAutoDetectUnicode(true); // 构造HTTP响应 responseStream << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/html\r\n" << "Connection: close\r\n" << "\r\n" << "

    Hello, " << value << "! (POST)

    "
    ; // 刷新并等待数据发送完毕 socket->flush(); socket->waitForBytesWritten(); } }; #endif // MYHTTPSERVER_H
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
  • 相关阅读:
    经典的设计模式6——桥接模式
    高级二-十进制转换算法
    java-php-python-ssm信息学院网站分析计算机毕业设计
    从上升边和带宽关系的两种公式说起
    使用Python语言制作贪吃蛇游戏,并制作成为exe可执行文件
    使用 sklearn 进行数学建模的通用模板
    C进阶 - 程序的编译(预处理操作) + 链接
    java参数传值
    机器人C++库(12) Robotics Library 之路径规划算法:PRM、RRT、EET算法
    【Windows编程】windows窗口创建过程详解
  • 原文地址:https://blog.csdn.net/hhw_hhw/article/details/134420953