📚 系列:C++ 工程实践 第 06 篇 ⏱ 阅读约 8 分钟
📌 一句话总结
本文用「起服务 → 看请求 → 写客户端 → 跑通验证」四步,带你吃透 cpp-httplib 的 server.cc 与 client.cc:① 一个服务同时提供接口和静态文件;② 看清 Request 的六类信息;③ 客户端会带查询参数和请求头发 GET。
文章目录
-
- 1. 为什么选 cpp-httplib:一个头文件解决服务端 + 客户端
- 2. 环境准备:一个头文件 + 一份 makefile
- 3. 服务端 server.cc:注册路由 + 挂载静态目录
- 4. Request 六件套:请求里到底有什么
- 5. 客户端 client.cc:带参数和请求头发 GET
- 6. 跑通验证:make、curl、client 三连
- 7. 踩坑与总结
1. 为什么选 cpp-httplib:一个头文件解决服务端 + 客户端
先说结论:它把 HTTP 服务端和客户端塞进一个 httplib.h,不用装库、不用写构建系统,适合「给 C++ 程序加个接口」这种场景。
| 引入成本 | 单个头文件 | 需装库/链接 | 依赖 Boost | 需编译框架 |
| 服务端 | ✅ 内置 | ❌ 只管客户端 | ✅ 但 API 底层 | ✅ |
| 客户端 | ✅ 内置 | ✅ 强项 | ✅ | ⚠️ 有限 |
| HTTPS | ✅(OpenSSL 宏) | ✅ | ✅ | ✅ |
这篇文章不再空讲 API,直接对着两个文件讲:server.cc 起一个能提供接口 + 静态页面的服务,client.cc 发一个带查询参数和请求头的 GET。跑通这两段,日常开发就够用了。
请求进来之后,服务端的处理顺序可以用一张图记住:
#mermaid-svg-Fj3qoglYWmrTe6aF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fj3qoglYWmrTe6aF .error-icon{fill:#552222;}#mermaid-svg-Fj3qoglYWmrTe6aF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fj3qoglYWmrTe6aF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fj3qoglYWmrTe6aF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fj3qoglYWmrTe6aF .marker.cross{stroke:#333333;}#mermaid-svg-Fj3qoglYWmrTe6aF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fj3qoglYWmrTe6aF p{margin:0;}#mermaid-svg-Fj3qoglYWmrTe6aF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster-label text{fill:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster-label span{color:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster-label span p{background-color:transparent;}#mermaid-svg-Fj3qoglYWmrTe6aF .label text,#mermaid-svg-Fj3qoglYWmrTe6aF span{fill:#333;color:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF .node rect,#mermaid-svg-Fj3qoglYWmrTe6aF .node circle,#mermaid-svg-Fj3qoglYWmrTe6aF .node ellipse,#mermaid-svg-Fj3qoglYWmrTe6aF .node polygon,#mermaid-svg-Fj3qoglYWmrTe6aF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Fj3qoglYWmrTe6aF .rough-node .label text,#mermaid-svg-Fj3qoglYWmrTe6aF .node .label text,#mermaid-svg-Fj3qoglYWmrTe6aF .image-shape .label,#mermaid-svg-Fj3qoglYWmrTe6aF .icon-shape .label{text-anchor:middle;}#mermaid-svg-Fj3qoglYWmrTe6aF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Fj3qoglYWmrTe6aF .rough-node .label,#mermaid-svg-Fj3qoglYWmrTe6aF .node .label,#mermaid-svg-Fj3qoglYWmrTe6aF .image-shape .label,#mermaid-svg-Fj3qoglYWmrTe6aF .icon-shape .label{text-align:center;}#mermaid-svg-Fj3qoglYWmrTe6aF .node.clickable{cursor:pointer;}#mermaid-svg-Fj3qoglYWmrTe6aF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Fj3qoglYWmrTe6aF .arrowheadPath{fill:#333333;}#mermaid-svg-Fj3qoglYWmrTe6aF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Fj3qoglYWmrTe6aF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Fj3qoglYWmrTe6aF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fj3qoglYWmrTe6aF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Fj3qoglYWmrTe6aF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fj3qoglYWmrTe6aF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster text{fill:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF .cluster span{color:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Fj3qoglYWmrTe6aF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Fj3qoglYWmrTe6aF rect.text{fill:none;stroke-width:0;}#mermaid-svg-Fj3qoglYWmrTe6aF .icon-shape,#mermaid-svg-Fj3qoglYWmrTe6aF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fj3qoglYWmrTe6aF .icon-shape p,#mermaid-svg-Fj3qoglYWmrTe6aF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Fj3qoglYWmrTe6aF .icon-shape .label rect,#mermaid-svg-Fj3qoglYWmrTe6aF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fj3qoglYWmrTe6aF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Fj3qoglYWmrTe6aF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Fj3qoglYWmrTe6aF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
浏览器 / curl / client
httplib::Server
命中路由?
HelloWorld 或 numbers
去 webroot 找文件
Response
图 1:路由优先,静态文件兜底
2. 环境准备:一个头文件 + 一份 makefile
先说结论:把 httplib.h、server.cc、client.cc、webroot/ 放在同一个目录,配一份 makefile 就能编译。
目录结构:
httplib/
├── httplib.h # 单头文件库
├── server.cc # 服务端
├── client.cc # 客户端
├── makefile # 构建脚本
└── webroot/
└── index.html # 静态页面
httplib.h 从仓库 release 页下载,放到工程目录即可。建议固定一个 release 版本(比如 v0.18.x)并提交进仓库,避免不同机器拿到不同版本、行为对不上。
all: client server
client: client.cc
g++ -std=c++17 -I. $^ -o $@ -lpthread
server: server.cc
g++ -std=c++17 -I. $^ -o $@ -lpthread
clean:
rm -rf server client
.PHONY: all clean
ℹ️ 两个小细节
① make 默认会依次找 GNUmakefile、makefile、Makefile,所以小写 makefile 也能直接 make;② 代码里写的是 #include <httplib.h>(尖括号),编译器不会搜当前目录,必须加 -I.,否则报「找不到 httplib.h」。
3. 服务端 server.cc:注册路由 + 挂载静态目录
先说结论:服务端只有四步——实例化 Server、注册路由、挂载静态目录、listen。
完整的 server.cc(把原文件里被注释的骨架补全后):
#include <httplib.h>
#include <iostream>
// 处理 GET /hi 请求:打印请求信息,返回一段 HTML
void HelloWorld(const httplib::Request &req, httplib::Response &rsp) {
std::cout << req.method << std::endl; // 请求方法,如 GET
std::cout << req.path << std::endl; // 请求路径,如 /hi
std::cout << req.body << std::endl; // GET 请求的 body 通常为空
for (const auto &header : req.headers) { // 请求头(map)
std::cout << header.first << " = " << header.second << std::endl;
}
for (const auto ¶m : req.params) { // 查询参数(multimap,可能有同名)
std::cout << param.first << " = " << param.second << std::endl;
}
const std::string html_body =
"<html><body><h1>Hello World</h1></body></html>";
rsp.set_content(html_body, "text/html"); // 正文 + Content-Type
rsp.status = 200; // 显式返回 200
}
int main() {
httplib::Server svr; // ① 实例化服务器
svr.Get("/hi", HelloWorld); // ② 静态路由
// ③ 正则路由:/numbers/123,matches[1] 是捕获到的数字
svr.Get(R"(/numbers/(\\d+))",
[](const httplib::Request &req, httplib::Response &rsp) {
std::cout << req.method << " " << req.path << std::endl;
for (const auto &match : req.matches) {
std::cout << match << std::endl; // matches[0] 是完整路径
}
rsp.status = 200;
});
svr.set_mount_point("/", "./webroot"); // ④ 挂载静态目录
svr.listen("0.0.0.0", 9000); // ⑤ 启动,阻塞直到 stop
return 0;
}
路由与静态目录的优先级:先匹配注册的路由,匹配不到才去静态目录找文件。所以 /hi、/numbers/123 走接口,/、/index.html 走 webroot。set_mount_point 的目录是相对进程工作目录的,必须从含 webroot/ 的目录启动服务,否则要写绝对路径。
响应这边,set_content 会同时设置正文和 Content-Type,浏览器据此决定按 HTML 渲染还是当纯文本显示;rsp.status 不写时默认是 200,但显式写出来,后面加参数校验、返回 400/500 时才有统一的出口习惯。
4. Request 六件套:请求里到底有什么
先说结论:req.method、req.path、req.body 看单值,req.headers、req.params、req.matches 拿来遍历。
| method | std::string | 请求方法:GET / POST / PUT / DELETE |
| path | std::string | 请求路径,不含查询串 |
| body | std::string | 请求正文,POST 时才有内容 |
| headers | Headers(multimap) | 请求头,Connection、Content-Type 等 |
| params | Params(multimap) | 查询参数,GET 时来自 ?a=1&b=2,POST 表单也会进这里 |
| matches | Match | 仅正则路由有效,matches[0] 是完整路径,[1] 起是捕获组 |
两个容易忽略的点:
- params 是 multimap,同名参数(?tag=a&tag=b)会保留多条,用范围 for 遍历最稳,不要用 req.params["tag"]。
- matches 只在正则路由里有效;普通路由(/hi)里它是空的,别写进去取值。
POST 请求的判断逻辑要麻烦一点:application/x-www-form-urlencoded 的表单会被解析进 req.params,JSON 之类的请求体则原样留在 req.body 里,用之前先看 req.get_header_value("Content-Type"),不要拿 req.body 当万能入口。需要按字段名取请求头时用 req.get_header_value("Connection"),它比遍历 headers 更直接。
5. 客户端 client.cc:带参数和请求头发 GET
先说结论:Client + Headers + Params + Get,拿到 Result 先判空再读 status 和 body。
#include <httplib.h>
#include <iostream>
int main() {
// ① 实例化客户端:协议 + IP + 端口;跨机访问换成 192.168.x.x
httplib::Client cli("http://127.0.0.1:9000");
cli.set_connection_timeout(2, 0); // 连接超时 2 秒,别用默认的无限等
// ② 请求头:本次请求结束后关闭连接
httplib::Headers headers{{"Connection", "closed"}};
// ③ 查询参数:Get 时会自动拼成 /hi?name=lisi&age=18
httplib::Params params{{"name", "lisi"}, {"age", "18"}};
// ④ 发 GET;rsp 是响应结果的智能指针
auto rsp = cli.Get("/hi", params, headers);
// ⑤ 判空 + 判状态码,再取正文
if (rsp && rsp->status == 200) {
std::cout << rsp->body << std::endl;
} else {
std::cerr << "request failed: "
<< (rsp ? std::to_string(rsp->status)
: httplib::to_string(rsp.error()))
<< std::endl;
return 1;
}
return 0;
}
Get 的完整签名是 Get(path, params, headers),参数顺序别写反;rsp 是 Result(智能指针),if (rsp) 判的是「请求有没有成功」,rsp->status 才是 HTTP 状态码,两者要分开判断。
Client 内部维护连接,可以复用;但它不适合被多个线程同时调用,多线程场景建议每个线程一个实例,或者给共享实例加锁。超时也一定要设,默认不设超时的话,对端半开连接会让客户端一直等下去。
6. 跑通验证:make、curl、client 三连
先说结论:make 之后先起服务,再用 curl 和 client 分别验证,服务端日志会打印每一类请求信息。
make # 生成 server 和 client
./server & # 后台起服务,监听 9000
用 curl 分别打三类请求:
# 1. 静态路由,带两个查询参数
curl -i "http://127.0.0.1:9000/hi?name=lisi&age=18"
# 2. 正则路由,数字会被 matches 捕获
curl -i "http://127.0.0.1:9000/numbers/123"
# 3. 静态文件,读取 webroot/index.html
curl -i "http://127.0.0.1:9000/"
服务端控制台会依次打印:
GET
/hi
name = lisi
age = 18
GET /numbers/123
/numbers/123
123
客户端再跑一次:
./client
<html><body><h1>Hello World</h1></body></html>
浏览器直接打开 http://127.0.0.1:9000/,看到的就是 webroot/index.html 的内容——一个服务同时提供接口和静态页面,这就是 cpp-httplib 最省事的地方。
服务端日志的价值也在这里:它把「谁、用什么方法、访问了哪条路径、带了什么参数」原样打了出来,接口对不上时,先看日志比抓包快。
7. 踩坑与总结
先说结论:坑主要集中在「编译参数、监听阻塞、静态目录、跨机地址」四件事上。
| 没加 -I. | 编译报找不到 httplib.h | makefile 里加 -I.(尖括号 include 不搜当前目录) |
| 忘记 -lpthread | 链接报 pthread 相关符号未定义 | Linux 加 -lpthread;Windows/MinGW 再加 -lws2_32 |
| 以为 listen 会返回 | 程序卡住不往下走 | listen 是阻塞的,用 & 放后台或另起线程 |
| 静态目录 404 | / 访问不到页面 | 从含 webroot/ 的目录启动,或给绝对路径 |
| 请求路由被静态文件抢走 | 接口返回了文件内容 | 路由优先于静态目录;先注册接口再挂载目录 |
| 跨机访问失败 | 本机 curl 通、另一台不通 | 客户端地址改成服务端真实 IP,检查防火墙/端口 |
| 端口被占用 | 启动即退出,提示 bind 失败 | 换端口,或先 ss -lntp | grep 9000 查占用进程 |
⚠️ 生产环境别照搬
这个例子是教学骨架:没有鉴权、没有限流、没处理 HTTPS。真正上线时,鉴权放网关或中间件、TLS 交给 Nginx、接口加参数校验和统一错误码,再补上 set_payload_max_length 这类保护。
📝 小结
① 服务端四步:实例化、注册路由、挂载静态目录、`listen`。
② Request 六件套:method / path / body 看单值,headers / params / matches 做遍历。
③ 客户端五步:建 Client、加 Headers、拼 Params、发 Get、先判空再读 status 和 body。
📦 项目源码
本文示例所在的 C++ 微服务脚手架(点播系统):https://github.com/syagain-zsx/cpp-microservice-scaffold-vod-system
🔜 下一篇
《WebSocket 教程:websocketpp 写 echo 服务端》——从 HTTP 请求响应走向实时通信。关注专栏不迷路。
🔗 参考
[1] cpp-httplib 仓库与示例:https://github.com/yhirose/cpp-httplib
[2] README(路由、静态文件、客户端用法):README.md
[3] GNU Make 手册:https://www.gnu.org/software/make/manual/
网硕互联帮助中心




评论前必须登录!
注册