- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
本篇技术指南以 cpp-httplib 的CustomRoute()API 为核心,讲解如何在默认只接受标准 HTTP 方法的服务器上,注册并路由PROPFIND、PROPPATCH、MKCOL、REPORT、SUBSCRIBE等扩展方法,并配合OPTIONS能力通告实现可用的 WebDAV / UPnP 服务。读完本文,你将掌握自定义方法的注册写法、内容读取器(Content Reader)流式处理请求体、注册校验与启动失败的联动机制,以及对应的源码级实现依据。
为什么需要自定义 HTTP 方法
HTTP 协议(RFC 9110)定义了GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等方法,但现实世界中的许多协议会在其上扩展新方法:
- WebDAV(RFC 4918):
PROPFIND、PROPPATCH、MKCOL、COPY、MOVE、LOCK、UNLOCK等,用于文件与目录的分布式管理; - UPnP:
SUBSCRIBE/NOTIFY等,用于事件订阅与推送; - 其他扩展:
REPORT(WebDAV 的搜索与版本控制扩展)、M-SEARCH(SSDP)等。
cpp-httplib 的默认行为是:服务器对不认识的 HTTP 方法直接以400 Bad Request拒绝。这一拦截发生在请求行解析阶段——httplib.h 中parse_request_line()的逻辑是:当方法不在内置方法集合(builtin_methods())中、同时也没有通过CustomRoute()注册对应处理器时,立即输出Error::InvalidHTTPMethod错误日志并返回 false:
// httplib.h const auto &methods = builtin_methods(); if (methods.find(req.method) == methods.end() && !find_custom_entry(req.method)) { output_error_log(Error::InvalidHTTPMethod, &req); return false; }因此,"注册了处理器"本身就是"接受该方法"的唯一开关——这是理解CustomRoute()语义的关键:注册即放行,不注册即拒绝。
CustomRoute() 基本用法
调用Server::CustomRoute(method, pattern, handler)即可为扩展方法注册路由。方法名、URL 模式的写法与Get()等内置方法完全一致,正则表达式和路径参数(path parameters)都可以直接使用:
svr.CustomRoute("PROPFIND", "/dav/:id", [](const httplib::Request &req, httplib::Response &res) { // 请求体与普通方法一样可以直接读取 auto id = req.path_params.at("id"); res.status = httplib::StatusCode::MultiStatus_207; res.set_content(build_multistatus(req.body), "application/xml"); });这段代码演示了三个关键点:
- 模式匹配复用同一套机制:
/dav/:id中的:id路径参数会被解析进req.path_params,与Get("/dav/:id", ...)的行为完全一致;正则表达式模式(如/dav/.*)同样可用; - 请求体按需读取:
req.body中已经装载了完整请求体,和Post()处理器中的用法没有差别; - 自定义状态码:
httplib::StatusCode::MultiStatus_207对应 WebDAV 的207 Multi-Status,用于返回集合操作的部分成功结果。
从源码看,CustomRoute()存在两个重载,签名分别是Handler与HandlerWithContentReader(见 httplib.h),前者把处理器挂到custom_handlers_表,后者挂到handlers_for_content_reader表,对应下文"流式读取"的用法:
inline Server &Server::CustomRoute(const std::string &method, const std::string &pattern, Handler handler); inline Server &Server::CustomRoute(const std::string &method, const std::string &pattern, HandlerWithContentReader handler);注册校验机制:从 is_token 到 is_valid()
并非任何字符串都能作为方法名注册。CustomRoute()在真正插入路由表之前,会经过custom_entry_for_registration()的严格校验(见 httplib.h):
if (!detail::fields::is_token(method) || builtin_methods().count(method)) { output_error_log(Error::InvalidHTTPMethod, nullptr); has_invalid_registration_ = true; return nullptr; }方法名必须是合法的 HTTP Token(RFC 9110)
is_token()要求方法名非空,且每个字符都落在 token 字符集内。具体的判定实现在 httplib.h:字母数字,以及! # $ % & ' * + - . ^ _| ~等特殊字符被允许;空格、制表符、斜杠、逗号、冒号、括号、控制字符(如\x01`)都会导致校验失败。
内置方法禁止注册
builtin_methods()(见 httplib.h)维护了一个 10 元素集合:
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, PRI这些方法被排除在CustomRoute()之外的原因在源码注释中解释得很清楚:
GET、HEAD、POST、PUT、DELETE、OPTIONS、PATCH由routing()中的 if/else 分发链先行处理,注册到自定义表也永远不会被触发;CONNECT(隧道建立)、TRACE(请求回显)、PRI(HTTP/2 连接前导)承载着协议级语义,库本身不做路由。
因此,这些方法必须使用专用的Get()、Post()、Put()、Options()等 API 注册,而不是塞进CustomRoute()。
拒绝注册的后果:is_valid() 与 listen() 联动
一旦某个注册被拒绝,has_invalid_registration_标志位会被置位(该标志在listen()前写入、由同一线程的is_valid()读取,无需同步)。此后:
svr.is_valid()返回false;listen()调用失败,返回false。
这套机制保证了服务器绝不会带着一个永远无法触发的处理器启动。更值得注意的是"粘性"行为:拒绝状态不会被后续的合法注册清除。在 test/test.cc 的RejectionIsSticky测试中,依次注册PROPFIND(合法)、GET(非法)、MKCOL(合法)后,is_valid()仍然为false——也就是说,一次非法注册就足以让整个服务器拒绝启动,注册顺序、后续合法注册都无法挽回。
测试 test/test.cc 完整覆盖了这套校验矩阵:
| 测试用例 | 验证内容 |
|---|---|
RejectsBuiltInMethods | 10 个内置方法逐个注册均被拒绝,且is_valid()、listen()均为 false |
RejectsNonTokenMethods | 空字符串、PRO PFIND、含\t/,:(及控制字符的名称均被拒绝 |
AcceptsWebDavAndUpnpMethods | PROPFIND、PROPPATCH、MKCOL、COPY、MOVE、LOCK、UNLOCK、REPORT、SUBSCRIBE均可注册成功 |
ContentReaderOverloadRejectsBuiltInMethods | Content Reader 重载同样拒绝内置方法 |
ReportsRejectionToErrorLogger | 拒绝时通过set_error_logger()回调收到Error::InvalidHTTPMethod |
用 OPTIONS 通告服务器能力
WebDAV 客户端在发起任何操作之前,会先发送OPTIONS请求探测服务器能力。cpp-httplib 不会自动生成DAV:头或Allow头,这两者必须由你自己返回。如果遗漏这一步,即使PROPFIND处理器工作正常,客户端也会因能力通告缺失而拒绝继续交互。
svr.Options("/dav/.*", [](const httplib::Request &req, httplib::Response &res) { res.set_header("DAV", "1"); res.set_header("Allow", "OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL"); });DAV: 1表明服务器支持 WebDAV(RFC 4918)的 class 1 能力;Allow头列出服务器实际接受的完整方法清单,需要与你在代码中注册的方法保持一致,客户端会据此决定后续行为。
注意这里使用的是svr.Options(...)(内置方法专用 API),而不是CustomRoute("OPTIONS", ...)——因为OPTIONS属于内置方法,无法通过CustomRoute()注册。
用 Content Reader 流式读取请求体
大型 WebDAV 请求(例如PROPFIND携带深目录请求、REPORT携带查询表达式)可能包含体积可观的 XML。如果不想一次性把整个请求体载入内存,可以使用CustomRoute()的Content Reader 重载,用法与Post()的 Content Reader 版一致:
svr.CustomRoute("REPORT", "/dav/.*", [](const httplib::Request &req, httplib::Response &res, const httplib::ContentReader &content_reader) { content_reader(& { // 逐块处理请求体数据 return true; // 返回 false 可中止读取 }); res.status = httplib::StatusCode::MultiStatus_207; });content_reader接受一个接收回调,框架会分块回调(data, data_length),处理器在回调内逐步消费数据;回调返回false可提前终止读取。
一个容易被忽略的实现细节值得展开:Content Reader 路由在请求完全没有请求体时也会触发。routing()中的判定条件(见 httplib.h)是:
if (detail::expect_content(req) || (custom && !custom->handlers_for_content_reader.empty())) {也就是说,只要某个自定义方法注册了 Content Reader 处理器,即使请求头既没有Content-Length也没有Transfer-Encoding,该处理器仍然会被调用。这对 WebDAV 有实际意义:RFC 4918 规定不带请求体的PROPFIND等价于allprop请求(返回全部属性)。如果缺少这一逻辑,body-less 的PROPFIND会跳过处理器直接落到 404。对应地,test/test.cc 的CustomMethodWithoutFraming测试专门验证了:发送不带任何 framing 信息的裸PROPFIND /dav请求,服务器应立即返回207 Multi-Status,而不是阻塞在等待 EOF 的读取上。
静态文件与 WebSocket:仍只走 GET / HEAD
自定义方法只影响 HTTP 方法路由,不影响其他子系统:
- 静态文件服务:
set_mount_point()/set_base_dir()挂载的静态文件仍只响应GET和HEAD(见 httplib.h 中routing()的文件处理分支:if ((req.method == "GET" || req.method == "HEAD") && handle_file_request(req, res))); - WebSocket 升级:同样以
GET为基础握手流程。
如果需要 WebDAV 风格的"虚拟文件系统",把PROPFIND映射到静态目录之外的业务逻辑由你自己实现,库只负责路由分发。
边界与责任:库只路由,不实现协议
最后需要明确 cpp-httplib 的能力边界:CustomRoute()提供的仅仅是"方法级别的路由"。如果你的服务要自称 WebDAV 实现,那么协议本体——207 Multi-Status响应 XML 的生成、Depth头的语义解释、Lock-Token/If头的锁管理、DAV:属性文档——全部需要你自己实现。此外:
- 方法名必须是合法的 HTTP token(RFC 9110),且必须在
listen()之前完成注册; GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH、PRI这 10 个内置方法不可通过CustomRoute()注册,请使用对应的专用方法;- 被拒绝的注册会让
is_valid()返回false、listen()失败,且该拒绝状态是"粘性"的,服务器绝不会带着永远无法触发的手柄启动。
延伸阅读
- 处理器注册的基础知识(路径匹配、正则、路径参数、Content Reader 语义)参见 S01. GET / POST / PUT / DELETE 处理器注册入门;
- 自定义方法的完整实现与校验逻辑见 httplib.h(
builtin_methods()/custom_entry_for_registration()/CustomRoute()/find_custom_entry())以及 httplib.h 的请求行校验; - 路由分发与 Content Reader 分支见 httplib.h;
- 全部注册校验与无 framing 请求的测试用例集中在 test/test.cc。
- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
相关推荐
cpp-httplib 自定义 HTTP 方法:用 CustomRoute() 接入 WebDAV、UPnP 等扩展协议
cpp httplib 自定义 HTTP 方法:用 CustomRoute 接入 WebDAV、UPnP 等扩展协议 导读 cpp httplib 默认只识别
后端网络在 Chromium 中使用 VSCode + rust-analyzer:gn 导出 Rust 项目与 IDE 开发指南
在 Chromium 中使用 VSCode + rust analyzer:gn 导出 Rust 项目与 IDE 开发指南 Rust 代码中大量类型信息会被编译
文档教程requests 怎么发送 MKCOL 等自定义 HTTP 动词?使用 request() 方法对接 WebDAV 服务
requests 怎么发送 MKCOL 等自定义 HTTP 动词?使用 request 方法对接 WebDAV 服务 对接 WebDAV 类服务时,经常会用到
后端网络通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考