返回「计算机、信息技术与工程」

cpp-httplib使用指南

cpp httplib 是一个轻量级的 C++ HTTP/HTTPS 库,用于构建 HTTP 客户端和服务器。

更多
Markdown 结构化数据
本文目录 40 个章节

cpp-httplib 是一个轻量级的 C++ HTTP/HTTPS 库,用于构建 HTTP 客户端和服务器。

cpp-httplib使用指南

GET 请求格式

GET 请求通常用于请求数据,它的特点是将请求参数附加在 URL 中,并且请求体通常为空。

请求行

GET /path/resource?key1=value1&key2=value2 HTTP/1.1
  • GET:请求方法,表示请求资源。
  • /path/resource?key1=value1&key2=value2:请求的路径及查询参数,查询参数通过 ?& 分隔。
  • HTTP/1.1:HTTP 协议版本。

请求头

Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
  • Host:目标服务器的域名。

  • User-Agent:客户端(如浏览器)的信息。

  • Accept:客户端可以处理的内容类型,如 text/html

  • 空行

    • 请求头后跟一个空行,表示请求头结束。
  • 请求体

    • GET 请求没有请求体,所有数据通过 URL 查询字符串传递。

GET 响应格式

状态行

HTTP/1.1 200 OK
  • HTTP/1.1:HTTP 协议版本。
  • 200:状态码,表示请求成功。
  • OK:状态描述。

响应头

Content-Type: text/html
Content-Length: 138
  • Content-Type:响应体的 MIME 类型,如 text/html 表示 HTML 页面。
  • Content-Length:响应体的长度,以字节为单位。

空行

  • 响应头后跟一个空行,表示响应头结束。

响应体

<h1>Hello, World!</h1>
  • 响应体包含实际的内容,如 HTML 页面、JSON 数据等。

POST 请求格式

POST 请求用于向服务器提交数据,如表单数据或文件上传。与 GET 不同,POST 请求的数据在请求体中传输,而不是通过 URL。

请求行

POST /path/resource HTTP/1.1
  • POST:请求方法,表示提交数据。
  • /path/resource:请求的路径,不包括查询参数。
  • HTTP/1.1:HTTP 协议版本。

请求头

Host: www.example.com
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
  • Content-Type:请求体的数据类型,如 application/x-www-form-urlencoded 表示表单数据,multipart/form-data 表示文件上传。
  • Content-Length:请求体的长度。

空行

  • 请求头后跟一个空行,表示请求头结束。

请求体

key1=value1&key2=value2
  • 请求体包含实际发送的数据,如表单字段或文件内容。

POST 响应格式

与 GET 响应类似,POST 响应包含状态行、响应头、空行和响应体。 状态行

HTTP/1.1 200 OK

响应头

Content-Type: application/json
Content-Length: 85
  • 空行
    • 响应头后跟一个空行,表示响应头结束。
  • 响应体
{
  "status": "success",
  "message": "Data received successfully",
  "data": {
    "key1": "value1",
    "key2": "value2"
  }
}
  • 响应体包含服务器返回的数据,通常是 JSON、HTML、XML 等格式。

常见响应状态码

1xx: 信息响应

  • 100 Continue:表示到目前为止一切正常,客户端应继续请求。
  • 101 Switching Protocols:服务器已接受客户端的请求,并正在更改协议。 2xx: 成功
  • 200 OK:请求已成功,服务器已返回所请求的数据。
  • 201 Created:请求已成功并且服务器创建了一个新的资源。
  • 202 Accepted:服务器已接受请求,但尚未处理完成。
  • 204 No Content:服务器成功处理了请求,但没有返回任何内容。 3xx: 重定向
  • 301 Moved Permanently:请求的资源已被永久移动到新的URL。
  • 302 Found:请求的资源临时从不同的URL响应请求。
  • 304 Not Modified:资源未被修改,客户端可以使用缓存的版本。 4xx: 客户端错误
  • 400 Bad Request:服务器无法理解请求,因为请求格式错误。
  • 401 Unauthorized:请求需要用户认证。
  • 403 Forbidden:服务器理解请求但拒绝执行。
  • 404 Not Found:服务器找不到请求的资源。
  • 405 Method Not Allowed:请求方法不被允许。 5xx: 服务器错误
  • 500 Internal Server Error:服务器在处理请求时遇到错误。
  • 501 Not Implemented:服务器不支持请求的方法。
  • 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效响应。
  • 503 Service Unavailable:服务器暂时无法处理请求,通常是由于超载或维护。
  • 504 Gateway Timeout:服务器作为网关或代理,没有及时从上游服务器接收到请求。

Client 基本功能

创建 HTTP 客户端

httplib::Client cli("httpbin.org");
  • 创建一个连接到 httpbin.org 的 HTTP 客户端。

发起 GET 请求

auto res = cli.Get("/hello");
  • 发送一个 GET 请求到 httpbin.org/hello,并返回响应对象 res

cli.Get(const char* path, const Headers& headers, ResponseHandler response_handler, ContentReceiver content_receiver) 最完整的形式,既可以指定请求头,还可以添加两个回调函数:

auto res = cli.Get("/stream", {},
  [&](const httplib::Response &response) {
    // Handle response headers
    return true;
  },
  [&](const char *data, size_t data_length) {
    // Handle data chunks
    return true;
  });
  • response_handler:在接收到服务器的响应头时调用。
  • content_receiver:在接收到响应体的每个数据块时调用。

知识补充 --- lambda 表达式的捕获列表: 在 C++ 中,lambda 表达式的方括号部分(也称为捕获列表)用来指定 lambda 表达式可以捕获并使用的外部变量。方括号中的内容决定了 lambda 表达式如何访问它所在的作用域中的变量。

捕获列表 [&, data] 的含义:

  1. &(按引用捕获)
    • 捕获列表中的 & 表示按引用捕获外部作用域中的所有变量。这意味着 lambda 表达式可以通过引用访问其作用域中的所有变量,因此可以修改这些变量的值。
    • 例如,如果外部作用域中有一个变量 counter,那么在 lambda 中对 counter 的修改会直接影响到外部的 counter 变量。
  2. data(按值捕获)
    • 捕获列表中的 data 指定了一个特定的变量 data,并且是按值捕获。这意味着 lambda 表达式内部有一个 data 的副本,lambda 内对 data 的修改不会影响到外部的 data 变量。
    • 在这段代码中,data 是一个指针,按值捕获意味着 lambda 捕获了 data 指针的副本,而不是指针所指向的对象的副本。换句话说,lambda 内部可以使用 data 指针访问实际的字符串数据,但不能改变 data 指针本身指向的地址。

发起 POST 请求

auto res = cli.Post("/post", "body content", "text/plain");
  • 向指定路径发送 POST 请求,并附带请求体和 Content-Type

PUT请求

res = cli.Put("/resource/foo", "text", "text/plain"); 这行代码使用 cpp-httplib 库中的 httplib::Client 类向服务器发送了一个 HTTP PUT 请求。

HTTP 的 PUT 方法通常用于更新服务器上的某个资源。它与 POST 不同,PUT 是幂等的,这意味着同样的 PUT 请求被多次发送,其结果应是相同的。如果资源 /resource/foo 已经存在,PUT 请求会更新它;如果资源不存在,则 PUT 请求通常会创建它。

当服务器收到这个 PUT 请求时,它会:

  1. 检查 URL /resource/foo 对应的资源是否存在。
  2. 使用请求体中的 "text" 作为新的资源内容,替换或创建资源。
  3. 返回一个响应,通常带有状态码(如 200 OK 表示成功,或 201 Created 表示资源已创建)。

使用场景:

  • 更新资源:客户端向服务器发送更新请求,例如更新用户信息、配置文件或其他可修改的资源。
  • 创建资源:如果服务器支持在目标资源不存在时创建新资源,PUT 也可以用于创建新资源。

Client 配置选项

启用 Keep-Alive

cli.set_keep_alive(true);
  • 启用 HTTP Keep-Alive,保持与服务器的连接,避免每次请求都重新建立连接。

处理重定向

cli.set_follow_location(true);
auto res = cli.Get("/");
  • 自动跟随服务器返回的重定向(例如 301302)。

自定义请求头

auto res = cli.Get("/path", {{"Authorization", "Bearer token"}});
  • 在请求中添加自定义头信息。

分块传输响应处理

auto res = cli.Get("/stream", Headers(),
  [&](const Response &response) { return true; },
  [&](const char *data, size_t data_length) { return true; });
  • 使用回调函数逐块处理服务器返回的数据,适合处理大数据或流媒体内容。

Range 请求

auto res = cli.Get("/range/32", {httplib::make_range_header({{1, 10}})});
  • 使用 Range 头部,只请求服务器上资源的特定字节范围。

  • cli.Get("/range/32", {...}); 发起了一个 GET 请求,请求路径是 /range/32,它会返回一个长度为 32 字节的资源。

  • 第二个参数 {httplib::make_range_header({{1, 10}})} 是一个 Range 头的设置,表示请求服务器返回文件从字节 1 到字节 10 的内容。

Server基本功能

创建 HTTP 服务器

httplib::Server svr;

svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
    res.set_content("Hello World!", "text/plain");
});
svr.listen("localhost", 8080);
  • 创建一个 HTTP 服务器并监听 localhost:8080 端口,响应 /hi 路径的 GET 请求。

文件请求处理

svr.set_file_request_handler([](const httplib::Request &req, httplib::Response &res) {
  // Custom logic before serving a file
});
  • 处理静态文件请求前自定义处理逻辑。

错误处理

svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
  res.set_content("Error Status: %d", "text/html");
});
  • 当服务器返回非 2xx 状态码时,使用自定义错误处理函数生成错误响应。

预处理路由

在处理客户端请求之前,这个处理器会先执行,允许你在实际路由处理器之前对请求进行拦截和处理。

svr.set_pre_routing_handler([](const httplib::Request &req, httplib::Response &res) {
  if (req.path == "/hello") {
    res.set_content("world", "text/html");
    return httplib::Server::HandlerResponse::Handled;
  }
  return httplib::Server::HandlerResponse::Unhandled;
});

Server 高级功能

Multipart 数据处理

svr.Post("/multipart", [&](const httplib::Request &req, httplib::Response &res) {
  if (req.is_multipart_form_data()) {
    req.get_file_value("name1");
    // Handle file content
  }
});
  • 处理 multipart/form-data 类型的 POST 请求,适用于文件上传等场景。

Stream 数据传输

const size_t DATA_CHUNK_SIZE = 4;

svr.Get("/stream", [&](const httplib::Request &req, httplib::Response &res) {
  auto data = new std::string("abcdefg");

  res.set_content_provider(
    data->size(), "text/plain",
    [&, data](size_t offset, size_t length, httplib::DataSink &sink) {
      sink.write(&data->at(offset), std::min(length, DATA_CHUNK_SIZE));
      return true;
    },
    [data](bool success) { delete data; });
});
  • 使用 set_content_provider 实现数据流式传输,适合大文件或实时数据传输。

收发数据进阶

服务器端 (Server)

接受数据(处理POST)

1. 处理multipart/form-data

  • 用途:处理带有文件上传的表单数据。
svr.Post("/multipart", [&](const auto& req, auto& res) {
  auto size = req.files.size();  // 获取上传文件的数量
  auto ret = req.has_file("name1");  // 检查是否有名为 "name1" 的文件
  const auto& file = req.get_file_value("name1");  // 获取文件内容和元数据
});

2. 使用 ContentReader 处理内容

  • 用途:接收和处理大数据流或文件上传,适用于 multipart/form-data 以及普通的 POST 请求。
svr.Post("/content_receiver",
  [&](const Request &req, Response &res, const ContentReader &content_reader) {
    if (req.is_multipart_form_data()) {
      MultipartFormDataItems files;
      content_reader(
        [&](const MultipartFormData &file) {
          files.push_back(file);
          return true;
        },
        [&](const char *data, size_t data_length) {
          files.back().content.append(data, data_length);
          return true;
        });
    } else {
      std::string body;
      content_reader([&](const char *data, size_t data_length) {
        body.append(data, data_length);
        return true;
      });
    }
  });

content_reader(...)content_reader 是一个回调函数,用于读取和处理 multipart/form-data 请求的每个部分。它接收两个回调:

  • 第一个回调用于处理每个文件部分的元数据(如文件名、内容类型),并将其添加到 files 容器中。
  • 第二个回调用于处理文件的实际内容,将读取到的数据追加到相应文件的 content 字段中。
  • content_reader 会一直阻塞,直到所有表单字段和文件内容都被读取完成。

发送数据(处理GET)

1. 发送数据 - 分块+流式 ContentProvider

  • 用途:服务器分块或流式发送响应数据,适合大数据或长时间运行的请求。
#include "httplib.h"
#include <iostream>

const size_t DATA_CHUNK_SIZE = 4; // 每次发送的数据块大小

int main() {
    httplib::Server svr;

    svr.Get("/stream", [&](const httplib::Request &req, httplib::Response &res) {
        auto data = std::make_shared<std::string>("abcdefg");  // 需要发送的完整数据

        res.set_content_provider(
            data->size(), // 设置内容长度
            "text/plain", // 设置内容类型
            [data](size_t offset, size_t length, httplib::DataSink &sink) {
                // 计算本次要发送的数据长度
                size_t chunk_size = std::min(length, DATA_CHUNK_SIZE);

                // 发送数据块
                sink.write(data->data() + offset, chunk_size);

                // 返回 true 继续发送下一块,直到所有数据发送完成
                return true;
            },
            [data](bool success) {
                if (success) {
                    std::cout << "Data sent successfully!" << std::endl;
                } else {
                    std::cerr << "Failed to send data!" << std::endl;
                }
            }
        );
    });

    svr.listen("localhost", 8080);
    return 0;
}

cpp-httplib 内部自动管理 offsetlength。当你使用 content_provider 时,cpp-httplib 会根据每次调用 sink.write 时的数据长度自动更新 offset

也可以不通过参数设定数据的总长度:

svr.Get("/stream", [&](const Request &req, Response &res) {
  res.set_content_provider(
    "text/plain", // Content type
    [&](size_t offset, DataSink &sink) {
      if (/* there is still data */) {
        std::vector<char> data;
        // prepare data...
        sink.write(data.data(), data.size());
      } else {
        sink.done(); // No more data
      }
      return true;
    });
});

2. 分块传输编码 (Chunked Transfer Encoding)

  • 用途:处理数据流,使用 HTTP 的 Chunked Transfer Encoding,适合动态生成内容或长时间流式传输的情况。
svr.Get("/chunked", [&](const Request& req, Response& res) {
  res.set_chunked_content_provider(
    "text/plain",
    [](size_t offset, DataSink &sink) {
      sink.write("123", 3);
      sink.write("345", 3);
      sink.write("789", 3);
      sink.done(); // No more data
      return true;
    }
  );
});

客户端 (Client)

发送数据(调用POST)

1. 发送 multipart/form-data 数据

  • 用途:客户端发送带有文件或字段的 multipart/form-data 请求。
httplib::MultipartFormDataItems items = {
  { "text1", "text default", "", "" },
  { "text2", "aωb", "", "" },
  { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
  { "file2", "{\n  \"world\", true\n}\n", "world.json", "application/json" },
  { "file3", "", "", "application/octet-stream" },
};

auto res = cli.Post("/multipart", items);

multipart/form-data 请求的格式

请求行与请求头

POST /path HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
  • Content-Type: multipart/form-data 指定了请求体包含多个部分,并且这些部分之间用 boundary 分隔。
  • boundary: 是一个字符串,用于分隔请求体中的不同部分。它通常是由 ---- 和一个随机生成的字符串组成,如 ----WebKitFormBoundary7MA4YWxkTrZu0gW

请求体 请求体包含实际的数据,分为多个部分,每个部分由 boundary 分隔,并且每个部分的格式如下: 普通字段

--boundary
Content-Disposition: form-data; name="field_name"

field_value

文件字段

--boundary
Content-Disposition: form-data; name="file_field_name"; filename="filename.jpg"
Content-Type: image/jpeg

(二进制文件数据)

结束标记

--boundary--

假设你正在上传一个包含文本字段和文件的表单,表单字段包括 text_fieldfile_field。HTTP 请求可能如下所示:

POST /upload HTTP/1.1
Host: www.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="text_field"

sample text
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file_field"; filename="example.jpg"
Content-Type: image/jpeg

(binary data of example.jpg)
----WebKitFormBoundary7MA4YWxkTrZu0gW--

2. 使用 ContentProvider 发送数据

  • 用途:客户端流式发送大数据,通过 ContentProvider 动态提供数据块。
std::string body = ...;

auto res = cli.Post(
  "/stream", body.size(),
  [](size_t offset, size_t length, DataSink &sink) {
    sink.write(body.data() + offset, length);
    return true;
  },
  "text/plain");

3. 分块传输编码 (Chunked Transfer Encoding)

  • 用途:客户端使用 Chunked Transfer Encoding 发送数据流。
auto res = cli.Post(
  "/stream",
  [](size_t offset, DataSink &sink) {
    sink.os << "chunked data 1";
    sink.os << "chunked data 2";
    sink.os << "chunked data 3";
    sink.done();
    return true;
  },
  "text/plain");

接收数据(调用GET)

1. 使用 ContentReceiver 接收数据

std::string body;

auto res = cli.Get("/large-data",
  [&](const char *data, size_t data_length) {
    body.append(data, data_length);
    return true;
  });

2. 进度回调

httplib::Client cli("http://example.com");

std::string body;

auto res = cli.Get(
    "/large-data",
    [&](const char *data, size_t data_length) {
        // 数据接收回调
        body.append(data, data_length);
        return true; // 返回 true 表示继续接收数据
    },
    [&](uint64_t len, uint64_t total) {
        // 进度回调
        printf("%llu / %llu bytes => %d%% complete\n",
               len, total,
               (int)(len * 100 / total));
        return true; // 返回 true 表示继续下载
    }
);