mongoose库实现http文件上传

1. 预备知识

1.1 URL编解码

常用于url链接和application/x-www-form-urlencoded格式的请求体中对参数进行编码
由于url的参数的样子是key1=value1&key2=value2,如果key或者value中包含= &等字符,就会导致解析时混乱了,因此需要一种编码来把这些可能引起歧义的符号替换掉
例如:http://localhost/src/components/global/Checkbox.vue?type=style&index=0
这个链接中 ? 的后面就是参数部分,即 type=style&index=0
这是两个键值对,type值为style,index值为0
假如现在 type 的值为 a=b,那么参数部分最后组装成 type=a=b&index=0 ,可见已经有点歧义了,但由于&分割,兴许还能解析
如果再假设type的值为 a&c=d,那组装后是 type=a&c=d&index=0,显然这个字符串给程序去解析的话,天王老子来了也会被解析为三个部分:type值为a,c值为d,index值为0

1.2 请求体编码格式

Http协议中,请求体有多种格式,如:

  • application/x-www-form-urlencoded,相当常用的格式,即和url中的参数一样,是key=value格式的字符串,且这个字符串是经过url编码的,在解析之前需要进行url解码。
  • multipart/form-data,可以上传多个键值对/文件。具体格式下文将着重展开。
  • application/json,顾名思义,就是json格式的
  • application/xml,xml格式,即像HTML一般的标签
  • text/plain,文本
  • application/octet-stream,二进制数据,且仅能上传一个文件。如果传单个文件(图片、音视频)使用这个相当快乐,它并不需要解析,整个请求体就是文件,但需要使用其他方式上传文件的文件名等信息。

如果有请求体,则应该在请求头使用 Content-Type 说明使用的编码格式

1.3 form-data格式

如果请求体是form-data格式,则在请求头中,我们应该能找到 Content-Type 的值为 multipart/form-data 且它后面会带一个 boundary:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

这里boundary是解析请求体用的
我们先来看看form-data格式的请求体的样子:

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="myfile"; filename="hello.gif" filename*=UTF-8''hello.gif
Content-Type: image/gif


{
    
    二进制数据}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="mytext"

coolight
------WebKitFormBoundary7MA4YWxkTrZu0gW--

这个请求体示例中有两个部分:

  • 文件名为hello.gif的动态图
  • 一个键值对,键为mytext,值为coolight

刚刚在请求头的 Content-Type 中的boundary的值在请求体中是用来分割数据的,在boundary前加两个-,即:–{boundary},并用它独占一行作为分隔标志。
注意最后一行分割标志的后面还有两个-,即 --{boundary}–
我们先看示例的第二部分,即键值对 mytext=coolight
其格式如下,其中把换行(\r\n)标出:
注意换行 \r\n 也是格式的一部分

--{
    
    boundary}(\r\n)
Content-Disposition: form-data; name="{key}"(\r\n)
(\r\n)
{
    
    value}(\r\n)
--{
    
    boundary}

然后是第一部分,上传文件时的格式:
其中与键值对格式不同的是,在name后面多了个filename,再后面还有一个可选的filename*。这是因为如果filename里面包含中文等非ASCII字符时,因客户端和服务端的编码不同而导致解析时filename乱码,因此可能会多传一个filename*,指定使用的编码格式,如UTF-8,且注意它的值是编码方式后紧接着两个单引号’,然后直接是对应编码的filename,整个字符串两端没有双引号。
注意 行Content-Type 之后还有两行才是数据。

--{
    
    boundary}(\r\n)
Content-Disposition: form-data; name="{key}"; filename="{filename}"(\r\n)[; filename*={
    
    编码方式}''{
    
    对应编码的filename}]
Content-Type: {
    
    文件格式}(\r\n)
(\r\n)
(\r\n)
{
    
    二进制数据}(\r\n)
--{
    
    boundary}

2. mongoose 文件上传实现

由于mongoose并没有提供文件上传功能,因此需要我们自己构建文件上传过程

2.1 构建请求header

header至少包含以下3部分

  • “POST /upload HTTP/1.1\r\n” 说明是post 文件upload
  • “Content-Type: multipart/form-data; Boundary=” 可包含多个form-data
  • "Content-Length: "文件长度=文件头+文件长度+文件尾

2.2 构建body头

  • boundary: 加入一行分割表示一个form-data的开始
  • Content-Disposition: 需指定类型form-data, name是file及filename
  • Content-Type: 参考请求体编码格式,比如上传时jpg图片,那么这里就时image/jpeg

2.3 发送header和body头

2.4 循环发送文件流

2.5 发送body结束分割

单独一行boundary表示一个form-data的结束

3. 完整代码

#include <iostream>
#include "mongoose.h"
#include <string>

static const uint64_t s_timeout_ms = 1500;

static void ev_handler(struct mg_connection* conn, int ev, void* ev_data, void *fn_data) {
    
    
    if (ev == MG_EV_OPEN) {
    
    
        // Connection created. Store connect expiration time in c->data
        *(uint64_t*)conn->data = mg_millis() + s_timeout_ms;
    }
    else if (ev == MG_EV_POLL) {
    
    
        if (mg_millis() > *(uint64_t*)conn->data &&
            (conn->is_connecting || conn->is_resolving)) {
    
    
            mg_error(conn, "Connect timeout");
        }
    }
    else if (ev == MG_EV_CONNECT) {
    
    
        // Connected to server. Extract host name from URL
    }
    else if (ev == MG_EV_HTTP_MSG) {
    
    
        struct mg_http_message* hm = (struct mg_http_message*)ev_data;
        // 处理HTTP响应
        std::cout << "Response body: \n" << hm->body.ptr << std::endl;
        bool* done = (bool*)fn_data;
        *done = true;
    }
}
int main() {
    
    
    std::string url {
    
    "http://192.168.31.86:8081/upload"};

    // 读取要上传的文件
    FILE* fp = fopen("E:\\code\\Yolov5_Tensorrt_Win10-master\\pictures\\bus.jpg", "rb");
    if (fp == NULL) {
    
    
        std::cout << "Failed to open file" << std::endl;
        return 1;
    }
    //get file len
    fseek(fp, 0, SEEK_END);
    int file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    struct mg_mgr mgr;
    struct mg_connection* conn;
    mg_mgr_init(&mgr);

    // create http connection
    bool done = false;
    conn = mg_http_connect(&mgr, url.c_str(), ev_handler, &done);
    if (conn == NULL) {
    
    
        std::cout << "Failed to connect" << std::endl;
        return 1;
    }

    // Build HTTP request body
    std::string boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
    std::string body_header = "--" + boundary + "\r\n"
        "Content-Disposition: form-data; name=\"file\"; filename=\"bus.jpg\"\r\n"
        "Content-Type: image/jpeg\r\n\r\n";

    std::string body_end = "\r\n--" + boundary + "--\r\n";

    // Build HTTP headers
    struct mg_str host = mg_url_host(url.c_str());

    std::string headers = "POST /upload HTTP/1.1\r\n"
        "Host: "+ std::string(host.ptr) + "\r\n"
        "Connection: keep-alive\r\n"
        "Content-Type: multipart/form-data; Boundary=" + boundary + "\r\n"
        "Content-Length: " + std::to_string(body_header.length() + file_size + body_end.length()) + "\r\n\r\n";

    // Send HTTP request
    mg_send(conn, headers.c_str(), headers.length());
    mg_send(conn, body_header.c_str(), body_header.length());

    // 逐块读取文件并发送数据
    char buffer[1024] = {
    
     0 };
    size_t bytes_read = 0;
    while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
    
    
        mg_send(conn, buffer, bytes_read);
    }
    mg_send(conn, body_end.c_str(), body_end.length());

    fclose(fp);

    // 等待响应
    while (!done) {
    
    
        mg_mgr_poll(&mgr, 1000);
    }
    mg_mgr_free(&mgr);
    
    return 0;
}

预备知识参考链接:
https://blog.coolight.cool/http-cform-data%E8%A7%A3%E6%9E%90/?replytocom=41295

猜你喜欢

转载自blog.csdn.net/wyw0000/article/details/131434243
今日推荐