系列C 工程实践 第 06 篇 ⏱ 阅读约 8 分钟 一句话总结本文用「起服务 → 看请求 → 写客户端 → 跑通验证」四步带你吃透 cpp-httplib 的server.cc与client.cc① 一个服务同时提供接口和静态文件② 看清 Request 的六类信息③ 客户端会带查询参数和请求头发 GET。文章目录1. 为什么选 cpp-httplib一个头文件解决服务端 客户端2. 环境准备一个头文件 一份 makefile3. 服务端 server.cc注册路由 挂载静态目录4. Request 六件套请求里到底有什么5. 客户端 client.cc带参数和请求头发 GET6. 跑通验证make、curl、client 三连7. 踩坑与总结1. 为什么选 cpp-httplib一个头文件解决服务端 客户端先说结论它把 HTTP 服务端和客户端塞进一个httplib.h不用装库、不用写构建系统适合「给 C 程序加个接口」这种场景。维度cpp-httpliblibcurlBoost.BeastCrow引入成本单个头文件需装库/链接依赖 Boost需编译框架服务端✅ 内置❌ 只管客户端✅ 但 API 底层✅客户端✅ 内置✅ 强项✅⚠️ 有限HTTPS✅OpenSSL 宏✅✅✅这篇文章不再空讲 API直接对着两个文件讲server.cc起一个能提供接口 静态页面的服务client.cc发一个带查询参数和请求头的 GET。跑通这两段日常开发就够用了。请求进来之后服务端的处理顺序可以用一张图记住是否浏览器 / curl / clienthttplib::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 -stdc17 -I. $^ -o $ -lpthread server: server.cc g -stdc17 -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把原文件里被注释的骨架补全后#includehttplib.h#includeiostream// 处理 GET /hi 请求打印请求信息返回一段 HTMLvoidHelloWorld(consthttplib::Requestreq,httplib::Responsersp){std::coutreq.methodstd::endl;// 请求方法如 GETstd::coutreq.pathstd::endl;// 请求路径如 /histd::coutreq.bodystd::endl;// GET 请求的 body 通常为空for(constautoheader:req.headers){// 请求头mapstd::coutheader.first header.secondstd::endl;}for(constautoparam:req.params){// 查询参数multimap可能有同名std::coutparam.first param.secondstd::endl;}conststd::string html_bodyhtmlbodyh1Hello World/h1/body/html;rsp.set_content(html_body,text/html);// 正文 Content-Typersp.status200;// 显式返回 200}intmain(){httplib::Server svr;// ① 实例化服务器svr.Get(/hi,HelloWorld);// ② 静态路由// ③ 正则路由/numbers/123matches[1] 是捕获到的数字svr.Get(R(/numbers/(\d)),[](consthttplib::Requestreq,httplib::Responsersp){std::coutreq.method req.pathstd::endl;for(constautomatch:req.matches){std::coutmatchstd::endl;// matches[0] 是完整路径}rsp.status200;});svr.set_mount_point(/,./webroot);// ④ 挂载静态目录svr.listen(0.0.0.0,9000);// ⑤ 启动阻塞直到 stopreturn0;}路由与静态目录的优先级先匹配注册的路由匹配不到才去静态目录找文件。所以/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拿来遍历。成员类型用途methodstd::string请求方法GET / POST / PUT / DELETEpathstd::string请求路径不含查询串bodystd::string请求正文POST 时才有内容headersHeadersmultimap请求头Connection、Content-Type等paramsParamsmultimap查询参数GET 时来自?a1b2POST 表单也会进这里matchesMatch仅正则路由有效matches[0]是完整路径[1]起是捕获组两个容易忽略的点params是multimap同名参数?tagatagb会保留多条用范围 for 遍历最稳不要用req.params[tag]。matches只在正则路由里有效普通路由/hi里它是空的别写进去取值。POST 请求的判断逻辑要麻烦一点application/x-www-form-urlencoded的表单会被解析进req.paramsJSON 之类的请求体则原样留在req.body里用之前先看req.get_header_value(Content-Type)不要拿req.body当万能入口。需要按字段名取请求头时用req.get_header_value(Connection)它比遍历headers更直接。5. 客户端 client.cc带参数和请求头发 GET先说结论ClientHeadersParamsGet拿到Result先判空再读status和body。#includehttplib.h#includeiostreamintmain(){// ① 实例化客户端协议 IP 端口跨机访问换成 192.168.x.xhttplib::Clientcli(http://127.0.0.1:9000);cli.set_connection_timeout(2,0);// 连接超时 2 秒别用默认的无限等// ② 请求头本次请求结束后关闭连接httplib::Headers headers{{Connection,closed}};// ③ 查询参数Get 时会自动拼成 /hi?namelisiage18httplib::Params params{{name,lisi},{age,18}};// ④ 发 GETrsp 是响应结果的智能指针autorspcli.Get(/hi,params,headers);// ⑤ 判空 判状态码再取正文if(rsprsp-status200){std::coutrsp-bodystd::endl;}else{std::cerrrequest failed: (rsp?std::to_string(rsp-status):httplib::to_string(rsp.error()))std::endl;return1;}return0;}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-ihttp://127.0.0.1:9000/hi?namelisiage18# 2. 正则路由数字会被 matches 捕获curl-ihttp://127.0.0.1:9000/numbers/123# 3. 静态文件读取 webroot/index.htmlcurl-ihttp://127.0.0.1:9000/服务端控制台会依次打印GET /hi name lisi age 18 GET /numbers/123 /numbers/123 123客户端再跑一次./clienthtmlbodyh1Hello World/h1/body/html浏览器直接打开http://127.0.0.1:9000/看到的就是webroot/index.html的内容——一个服务同时提供接口和静态页面这就是 cpp-httplib 最省事的地方。服务端日志的价值也在这里它把「谁、用什么方法、访问了哪条路径、带了什么参数」原样打了出来接口对不上时先看日志比抓包快。7. 踩坑与总结先说结论坑主要集中在「编译参数、监听阻塞、静态目录、跨机地址」四件事上。坑现象正确做法没加-I.编译报找不到httplib.hmakefile 里加-I.尖括号 include 不搜当前目录忘记-lpthread链接报 pthread 相关符号未定义Linux 加-lpthreadWindows/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/