0.前言
cpp-httplib 是一个 header-only 的跨平台 HTTP/HTTPS 网络库,采用 MIT 开源协议,接口为阻塞调用。虽然功能简单,但也不用一堆依赖,C++ 的接口也比 libcurl 的 C 接口用起来顺手一点,文档也提供了不少参考代码,如果是做简单的 HTTP server 或者 client 还是值得一试的。
库里有两个重要的宏:
关闭异常:CPPHTTPLIB_NO_EXCEPTIONS,默认遇到错误逻辑是抛异常,可以通过该宏来关闭,但是关了之后没有别的机制来获知这个错误。
启用 OpenSSL 支持:CPPHTTPLIB_OPENSSL_SUPPORT,需要 OpenSSL 的库可以被链接,目前仅支持 1.1.1 和 3.0 版本。
还有个问题是,该库仅支持 HTTP 1.0 和 1.1,默认 1.1,如果要用 HTTP2 或 HTTP3 用别的库吧。
库地址:https://github.com/yhirose/cpp-httplib
本文代码: https://github.com/gongjianbo/MyTestCode/tree/master/Qt/TestQt_20230913_HttpLib
(为方便操作,用 Qt 库拖了个简单的界面)
1.基本使用
cpp-httplib 提供了 RESTful 风格的接口,操作起来比 libcurl 方便不少。
服务端:
httplib::Server svr;
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
std::cerr << "Server-error:" << req.method << "\t" << req.path << "\tstatus:" << res.status << std::endl;
std::string json = R"({"message":"path error"})";
res.set_content(json, "text/plain");
});
// 路由处理抛了异常,会被捕获,然后进入 set_error_handler 逻辑
svr.Get("/hello", [](const httplib::Request &req, httplib::Response &res) {
std::cerr << "Server-log:" << req.method << "\t" << req.path << "\t" << req.body << std::endl;
// 获取 url 中的参数
for (auto it = req.params.begin(); it != req.params.end(); it++)
{
std::cerr << "\t" << it->first << ": " << it->second << std::endl;
}
res.set_content(R"({"message":"get result"})", "appliation/json");
});
svr.Post("/hello", [](const httplib::Request &req, httplib::Response &res) {
std::cerr << "Server-log:" << req.method << "\t" << req.path << "\t" << req.body << std::endl;
res.set_content(R"({"message":"post result"})", "appliation/json");
});
svr.listen("localhost", 12345);
listen 之后就阻塞住循环处理请求了。
客户端:
httplib::Client cli("localhost", 12345);
// auto res = cli.Get("hello?argkey=agrvalue");
auto res = cli.Post("hello", R"({"formkey":"formvalue"})", "application/json");
if (res) {
std::cerr << "Status Code:" << res->status << std::endl;
std::cerr << "Header:\n";
for (auto it = res->headers.begin(); it != res->headers.end(); it++)
{
std::cerr << "\t" << it->first << ": " << it->second << std::endl;
}
std::cerr << "Body:\n" << res->body << std::endl;
}
接口有多个重载,可根据需求选用,作者提供了大量的示例。
2.文件上传下载
文件的上传有两种方式,小文件可以一次性读写,大文件可以用 Provider 流式读写。
服务端:
// 文件上传
svr.Post("/upload", [](const httplib::Request &req, httplib::Response &res, const httplib::ContentReader &content_reader) {
std::cerr << "Server-log: upload\t" << req.get_header_value("Content-Type") << std::endl;
// 二进制数据可以用:multipart/form-data 和 application/octet-stream
if (req.is_multipart_form_data()) {
httplib::MultipartFormDataItems files;
// 先拿到 file 信息,再流式读取
// 通过file信息创建文件,然后持续写data数据,结束后关闭文件
content_reader(
[&](const httplib::MultipartFormData &file) {
files.push_back(file);
std::cerr << "\tupload read " << file.filename << "\t" << file.content << std::endl;
return true;
},
[&](const char *data, size_t data_length) {
files.back().content.append(data, data_length);
std::cerr << "\tupload read:" << data_length << std::endl;
return true;
});
} else {
std::string body;
content_reader([&](const char *data, size_t data_length) {
body.append(data, data_length);
std::cerr << "\tupload read:" << data_length << std::endl;
return true;
});
std::cerr << "\tupload read " << body << std::endl;
}
res.set_content(R"({"message":"upload result"})", "appliation/json");
});
// 文件下载
svr.Get("/download/:id", [](const httplib::Request &req, httplib::Response &res) {
std::cerr << "Server-log: download\t" << req.path_params.at("id") << "\t" << req.get_header_value("Content-Type") << std::endl;
res.set_header("Cache-Control", "no-cache");
res.set_header("Content-Disposition", "attachment; filename=hello.txt");
res.set_chunked_content_provider(
"multipart/form-data", [](size_t offset, httplib::DataSink &sink) {
const char arr[] = "hello world";
auto ret = sink.write(arr + offset, sizeof(arr));
sink.done();
std::cerr << "\tdownload write:" << sizeof(arr) << std::endl;
return !!ret;
});
});
客户端:
void doUp()
{
httplib::Client cli = httplib::Client{"localhost", 12345};
#if 0
// 小文件一次性发送
httplib::MultipartFormData form;
form.name = "myfile";
form.content = "hello world";
form.filename = "hello.txt";
form.content_type = "multipart/form-data";
httplib::MultipartFormDataItems form_items;
form_items.push_back(form);
httplib::Result res = cli.Post("/upload", form_items);
#else
// 大文件用流式接口
// 打开文件后,provider回调读分片数据并发送,判断发完后调用done结束,传输完后关闭文件
httplib::MultipartFormDataProvider provider;
provider.name = "myfile";
provider.filename = "hello.txt";
provider.provider = [&](size_t offset, httplib::DataSink &sink){
// offset 是已发送的偏移量
const char arr[] = "hello world";
auto ret = sink.write(arr + offset, sizeof(arr));
std::cerr << "Client write:" << arr << std::endl;
// 发送完成
sink.done();
return !!ret;
};
provider.content_type = "multipart/form-data";
httplib::MultipartFormDataProviderItems provider_items;
provider_items.push_back(provider);
// header 的 Content-Type 会默认设置为 multipart/form-data,且自动加上 boundary
httplib::Result res = cli.Post("/upload", {}, {}, provider_items);
#endif
std::cerr << __FUNCTION__ << "\tpath:/upload\tres:" << (!!res) << std::endl;
// printResult(res);
}
void doDown()
{
httplib::Client cli = httplib::Client{"localhost", 12345};
#if 0
// 小文件一次性读取
httplib::Result res = cli.Get("/download/0");
if (res) {
// Content-Disposition: attachment; filename=hello.txt
std::string dispsition = res->get_header_value("Content-Disposition");
std::cerr << dispsition << std::endl;
}
#else
// 大文件用流式接口
httplib::Result res = cli.Get(
"/download/0",
[&](const httplib::Response &response) {
std::cerr << "Client read:" << response.status << std::endl;
return true;
},
[&](const char *data, size_t data_length) {
std::cerr << "Client read:" << std::string(data, data_length) << std::endl;
return true;
});
#endif
std::cerr << __FUNCTION__ << "\tpath:/download/0\tres:" << (!!res) << std::endl;
// printResult(res);
}
为了方便演示,我把文件读写省去了,固定用的 "hello world" 字符串当作文件内容。实测改为文件读写也是可用的,测试用 1G 的文件可以正常收发且未损坏。