云计算百科
云计算领域专业知识百科平台

cpp-httplib 实战:单头文件写 HTTP 服务与客户端

📚 系列: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++ 程序加个接口」这种场景。

维度cpp-httpliblibcurlBoost.BeastCrow
引入成本 单个头文件 需装库/链接 依赖 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 &param : 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/

赞(0)
未经允许不得转载:网硕互联帮助中心 » cpp-httplib 实战:单头文件写 HTTP 服务与客户端
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!