cpp-httplib的下载和使用
- 1.httplib 简介
- 2. httplib 使用
- 2.1 协议接口
- 2.2 双端接口
- 2.3 实际使用
- 3. 对Server中的Handler回调函数进行分析
- 4. 最后
1.httplib 简介
cpp-httplib
(也称为 httplib
)是一个基于 C++
的轻量级 HTTP
框架,它提供了简单易用的 API
,用于创建 HTTP
服务器和客户端。并且cpp-httplib
是采用的select
多路复用+线程池的方式响应请求,读取请求,放到业务线程池对于业务处理,同时还支持切换多路复用的方式。默认情况下,由于其广泛的支持,使用了select
系统调用。如果你希望cpp-httplib
使用poll
,可以通过设置CPPHTTPLIB_USE_POLL
实现。
2. httplib 使用
httplib
的安装也很简单,直接克隆库中的 httplib.h
到项目中即可(https://github.com/yhirose/cpp-httplib)。接口有点多,我先写出常用的接口声明和类声明(另外,该编译该库最好使用 g++7.3
)。
2.1 协议接口
首先 http 报文内会包含:
- 首部:请求方法(
GET、POS
T)、URL(<protocol>://<username>:<password>@<host>:<port>/<path>?<params>#<fragment>)
、协议版本 - 报头:
key-value\r\n
对 - 空行:
\r\n
- 正文:请求的数据或者返回的数据
而 http 报文还会分为请求报文(Request)和响应报文(Response),因此在 httplib
中存在以下两个类(只是作为归纳的两个简化的类,实际声明可能有很大的不同)。
//Request 类
struct MultipartFormDataMap
{
std::string name; //表单字段的名称, 用于标识表单数据
std::string content; //文件内容
std::string filename; //文件名称
std::string content_type; //文件类型
};
struct Request
{
//(1)请求行
std::string method; //请求方法
std::string path; //资源路径(不是所有的 URL, 通常是域名之后, 查询字符之前)
Params params; //查询字符串
std::string version; //协议版本
//(2)请求头部
Headers headers; //头部
//(3)请求正文
std::string body; //正文
MultipartFormDataMap files; //保存客户端上传的文件信息
Ranges ranges; //指定要获取资源范围, 会根据 Range 字段指定的范围来返回相应的资源内容, 一旦中断后重连, 就会从中断的位置继续下载文件, 而不需要重新下载整个文件
bool has_header(const char *key) const;
//检查请求头部是否包含指定键名的头部字段, 参数 key 是要检查的头部字段的键名
//如果请求头部中包含指定键名的头部字段, 则返回 true 否则返回 false
std::string get_header_value(const char *key, size_t id = 0) const;
//获取请求头部中指定键名的报头字段的值, 参数 key 是要获取的字段键名, 参数 id 是可选的
//若头部字段有多个同名键名, 则可通过 id 来指定获取其中一个
//如果请求头部中存在指定键名的头部字段, 则返回该字段的值, 否则返回空字符串
void set_header(const char *key, const char *val);
//用于设置请求头部中的一个新的头部字段或更新已存在的头部字段
bool has_file(const char *key) const;
//用于检查请求中是否包含指定键名的文件字段(表单 name)
MultipartFormData get_file_value(const char *key) const;
//用于获取请求中指定键名的文件字段的值(表单 name)
};
//Response 类
struct Response
{
//(1)状态行
std::string version; //协议版本
int status = -1; //状态码
std::string reason; //状态描述
//(2)响应头部
Headers headers; //响应头部
//(3)响应正文
std::string body; //响应体
std::string location; //重定向地址
//设置响应头部
void set_header(const char *key, const char *val);
//设置响应正文
void set_content(const std::string &s, const char *content_type);
};
2.2 双端接口
//Server 类
class Server
{
public:
//1.路由方法设置
//请求路由:Handler 是函数类型, 无返回值, 传入请求, 带出响应
using Handler = std::function<void(const Request&, Response&)>;
//请求路由数组:众多 Handler 类型函数的列表 std::regex 是正则表达式, 用于填充和路由方法匹配 http 请求资源路径(实际上就是请求中的 path), 以后就可以根据用户端请求的路由选择对应的路由函数进行请求处理(若没有对端就会收到 404)
using Handlers = std::vector<std::pair<std::regex, Handler>>;
//(2)线程池设置
//线程池成员, 其工作就是接受请求, 解析请求, 在映射表中查看是否有可以执行方法, 每接到链接请求, 就会把新的客户端连接抛入线程池中,
std::function<TaskQueue* (void)> new_task_queue;
//(3)请求方法设置, 通过下面接口, 针对不同的请求, 把路由方法添加到 Handlers 中
//注册处理 GET 请求的处理程序
Server &Get(const std::string &pattern, Handler handler);
//注册处理 POST 请求的处理程序
Server &Post(const std::string &pattern, Handler handler);
//注册处理 PUT 请求的处理程序
Server &Put(const std::string &pattern, Handler handler);
//注册处理 PATCH 请求的处理程序
Server &Patch(const std::string &pattern, Handler handler);
//注册处理 DELETE 请求的处理程序
Server &Delete(const std::string &pattern, Handler handler);
//注册处理 OPTIONS 请求的处理程序
Server &Options(const std::string &pattern, Handler handler);
//4.启动 HTTP 服务器
bool listen(const char* host, int port, int socket_flags = 0);
};
//Client 类
class Client
{
public:
//传入对端服务器的 ip 和 port
Client(const std::string &host, int port);
//发送 GET 请求到指定路径
Result Get(const char *path);
//发送 GET 请求到指定路径, 可附带头部信息, 并返回结果
Result Get(const char *path, const Headers &headers);
//发送 POST 请求到指定路径, 包含纯文本主体、内容长度、内容类型
Result Post(const char *path, const char *body, size_t content_length, const char *content_type);
//发送 POST 请求到指定路径,包含多部分表单数据项
Result Post(const char *path, const MultipartFormDataItems &items); //最后一个参数是一个文件数组
};
2.3 实际使用
接下来我们尝试使用一下上述的接口。
先写一个自动化脚本,方便多次代码编译。
# makefile
all:http_server http_client
http_server:http_server.cpp
g++ -o $@ $^ -std=c++11
http_client:http_client.cpp
.PHONY:clean
clean:
rm -fr http_server http_client
测试server端
#include <iostream>
#include "./1.Origin_source/Comm/httplib.h"
#include <string>
#include <cstdlib>
int main(int argc, char const* argv[])
{
using namespace httplib;
Server svr;
svr.Get(R"(/my_get)", [](const Request& req, Response& res)
{
std::cout << "a get request" << std::endl;
res.set_content("hello linux", "text/plain");
res.status = 200;
}
);
// 匹配请求路径与正则表达式并提取其捕获值
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1]; // 获取正则表达式中的内容
res.set_content(numbers, "text/plain");
});
svr.Post(R"(my_post)", [](const Request& req, Response & res)
{
std::cout << "a post request" << std::endl;
auto ret = req.has_file("file");
if (!ret)
{
std::cout << "not file upload!" << std::endl;
res.status = 404;
return;
}
const auto& file = req.get_file_value("file");
std::cout << "file.name:" << file.name << std::endl;
std::cout << "file.filename:" << file.filename << std::endl;
std::cout << "file.content_type:" << file.content_type << std::endl;
std::cout << "file.content:" << file.content << std::endl;
std::string message = "This file is ";
message += file.filename;
message += "-";
message += file.content_type;
message += "\n";
message += file.content;
res.set_content(message, "text/plain");
res.status = 200;
}
);
svr.listen("0.0.0.0", 8080);
return 0;
}
观察运行结果
#运行结果
$ ./http_server 选定的端口号
a get request.
测试POST接口,看看是否可以上传文件。
<!-- test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
</head>
<body>
<form action="http://这里填您的服务器公网ip:这里填您给服务端代码绑定的port/my_post" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file">
<input type="submit" value="提交文件">
</form>
</body>
</html>
先将这个网页写到text.txt文本中,再通过修改后缀的方式进行生成网页,并打开,将提前写好的后缀并为.txt的文本中写入一些数据进行打开。
运行结果
a post request
file.name:file
file.filename:my_post.txt
file.content_type:text/plain
file.content:hello,this is a post request
对客户端进行编写测试
//较完整的 http_client.cpp
#include <iostream>
#include <cstdlib>
#include <fstream>
#include "./1.Origin_source/Comm/httplib.h"
int main(int argc, char const* argv[]) {
using namespace httplib;
Client cli(argv[1], atoi(argv[2]));
//1.发送 GET 请求
auto res_get = cli.Get("/my_get");
if (res_get->status == 200) {
std::cout << "get success" << std::endl;
std::cout << "返回响应的状态码: " << res_get->status << std::endl;
std::cout << "返回响应具体内容: " << res_get->body << std::endl;
} else {
std::cout << "get error" << std::endl;
}
//2.发送 POST 请求
//读取文件内容
std::ifstream file("./example.txt", std::ios::binary);
if (!file.is_open()) {
std::cout << "open error" << std::endl;
return 1;
}
//设置content内容
std::string file_content = "hello linux";
MultipartFormData item;
item.name = "file"; //表单字段名
item.filename = "example.txt"; //作为文件名
item.content = file_content.c_str(); //文件内容
item.content_type = "text/plain"; //文件格式
MultipartFormDataItems items;
items.push_back(item);
//发送 POST 请求上传文件, 并且返回 res 响应
auto res = cli.Post("/my_post", items);
if (res && res->status == 200) { //注意 res 其实就是一个指向响应的智能指针
std::cout << "port success" << std::endl;
std::cout << "返回响应的状态码: " << res_get->status << std::endl;
std::cout << "返回响应具体内容: " << res->body << std::endl;
} else {
std::cout << "port error" << std::endl;
}
return 0;
}
3. 对Server中的Handler回调函数进行分析
这里我们再写测试Server的时候是把svr.listen()写到了最后一行,我们要知道编译器执行发方式是顺序执行,也就是说编译器执行代码的顺序是从上往下指向的(函数跳转这里我们不考虑),而我们的get和post方法是写在了listen监听套接字的起那面,那么也就是说注定get和post方法会在创建listen套接字之前就已经执行完毕了。那么大家再第一次接触httplib的时候会不会有这样的困惑:就是不应该是创建好监听套接字后才开始执行get和post方法吗?这样才符合只要有客户端来了请求就调用get或者post方法。
所以我们就要看看server到底是怎么实现get和post方法的。
inline Server &Server::Get(const char *pattern, Handler handler) {
get_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
return *this;
}
inline Server &Server::Post(const char *pattern, Handler handler) {
post_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
return *this;
}
从上面看,post和get都是都是调用的post_handlers_.push_back()插入到一个容器里面的。
Handlers get_handlers_;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
从这里我们可以看出来post_handlers_其实就是一个vector容器。于是这里我们就恍然大悟了,原来是httplib再指向到get和post方法的时候是将pattern和handler回调函数进行了一种映射存储起来了。也就是说在没有创建好listen监听套接字的时候,httplib已经将路径和需要执行的函数功能建立了映射关系。当创建好套接字后,一旦客户来了请求,那么就会去这样映射表中进行查询回调函数进行回调,然后返回执行结果。
所以综上所述,上面的执行一个个的get和post方法其实就是再创建一个个的映射关系。客户端进行请求其实就是再这张映射表中查询是否有相关的映射值。
4. 最后
本篇章是在原作者的基础上稍加上了我自己的对源码的理解,具体细节请看原作者内容源地址https://blog.csdn.net/m0_73168361/article/details/137931807
或者前往cpp-httplib开源仓库中详细查看httplib的使用https://gitcode.com/gh_mirrors/cp/cpp-httplib/overview?utm_source=artical_gitcode&index=bottom&type=card&webUrl