- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
导读
cpp-httplib 默认只识别GET、POST等内置 HTTP 方法,收到未知方法(如 WebDAV 的PROPFIND、UPnP 的SUBSCRIBE)会直接以400 Bad Request拒绝。本篇指南讲解如何通过Server::CustomRoute()注册任意符合 RFC 9110 token 规范的扩展方法,包括路径参数与正则匹配、流式读取请求体、配合OPTIONS通告能力,以及注册失败时的is_valid()防护机制。读完你将能够在 cpp-httplib 之上实现 WebDAV/自定义协议服务端。
背景:为什么需要自定义方法
HTTP 方法(Method)并非只有标准的那十个。RFC 4918(WebDAV)定义了PROPFIND、PROPPATCH、MKCOL、COPY、MOVE、LOCK、UNLOCK、REPORT等扩展方法;UPnP 设备则常使用SUBSCRIBE做事件订阅。cpp-httplib 的处理策略是:只接受注册过处理器的方法。对未注册的未知方法,服务端在解析请求行时就判定无效并返回400 Bad Request。
这一点可以从请求解析源码直接印证。在 httplib.h 的请求行解析逻辑中:
// A method outside the built-in set is accepted only when a handler has been // registered for it with CustomRoute(). 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()是否注册过对应处理器;路由分发阶段(httplib.h)同样只在find_custom_entry()找到匹配条目时才进入自定义方法分发,否则最终落到res.status = StatusCode::BadRequest_400。注册处理器本身,就是让服务端接受该方法的开关。
Basic usage:注册一个自定义方法
CustomRoute()的签名与内置的Get()/Post()保持一致,第一个参数是方法名,第二个是路径模式,第三个是处理器回调:
svr.CustomRoute("PROPFIND", "/dav/:id", [](const httplib::Request &req, httplib::Response &res) { // The request body is available as usual auto id = req.path_params.at("id"); res.status = httplib::StatusCode::MultiStatus_207; res.set_content(build_multistatus(req.body), "application/xml"); });模式匹配与Get()完全一致:/dav/:id这类路径参数会填入req.path_params,也可直接使用正则表达式模式。仓库测试 test/test.cc 分别验证了路径参数(/dav/:id匹配/dav/42并提取id = "42")与正则模式(R"(/dav-re/(\d+))"匹配/dav-re/123并捕获123)两种写法。
注册完成后,处理器内部可以像普通请求一样访问req.body、req.headers,并通过res设置状态码、响应头与响应体。上面示例中返回207 Multi-Status(httplib.h 中枚举为StatusCode::MultiStatus_207)并附上application/xml的多状态文档,就是 WebDAV 服务最常见的应答形态。
用 OPTIONS 通告你的能力
WebDAV 客户端在做任何实际操作前,会先发送OPTIONS询问服务器的能力集合。cpp-httplib不会自动生成DAV:响应头,也不会生成Allow头,这两者需要你在OPTIONS处理器中自行返回。忽略这一点,即使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表示服务器支持 RFC 4918 定义的 WebDAV 第 1 级能力;如果你的实现还覆盖了版本管理或访问控制,需要按规范相应调整该值。Allow头应如实列出服务器实际支持的方法,避免通告了未实现的方法。- 由于
OPTIONS本身就是内置方法,这里走的是Server::Options()(httplib.h),无需CustomRoute()。
以流式方式读取请求体
WebDAV 的PROPFIND/REPORT请求体常常是体积不小的 XML 文档。如果不想一次性把整个文档读进内存,可以使用CustomRoute()的ContentReader重载——与Post()上的内容读取器重载行为一致(httplib.h):
svr.CustomRoute("REPORT", "/dav/.*", [](const httplib::Request &req, httplib::Response &res, const httplib::ContentReader &content_reader) { content_reader(& { // Process it a chunk at a time return true; }); res.status = httplib::StatusCode::MultiStatus_207; });回调按块接收数据:每收到一块就调用一次 lambda,data/data_length指向当前块;返回true表示继续读取,返回false则中止。数据会在读取过程中被逐块消费,不会在req.body中整体缓冲。
需要注意一个与内置方法一致的细节:内容读取器路由在请求没有请求体时也会触发。从 httplib.h 的路由逻辑可以看到,只要注册了 content-reader 处理器,即便请求没有Content-Length/Transfer-Encoding也会走内容读取路径——因为 RFC 4918 规定无请求体的PROPFIND等价于allprop查询。对应测试见 test/test.cc,该测试确认了无请求体的REPORT请求不会落到 404。
注册规则与校验:Things to keep in mind
使用CustomRoute()时有几条硬性约束,全部在注册阶段强制执行:
- 方法名必须是合法的 HTTP token(RFC 9110)。
is_token()的实现见 httplib.h:方法名不能为空,只能由字母数字以及!#$%&'*+-.^_|~` 等 token 字符组成,不允许空格、制表符、斜杠、逗号、冒号、括号或控制字符。 - 内置方法不允许注册。
builtin_methods()(httplib.h)包含GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH、PRI十个方法。前七个(含PATCH)由路由中的 if/else 链直接分发,用CustomRoute()注册它们永远不可能被触发;CONNECT、TRACE、PRI虽无对应分支,但分别承载隧道建立、请求回显、HTTP/2 连接前言的协议级语义,库不负责路由。这些方法应使用各自专属的注册函数(Get()、Post()、Put()、Delete()、Options()、Patch()等)。 - 必须在调用
listen()之前完成注册。注册被拒绝会立即使is_valid()返回false,listen()随之失败,服务器根本不会启动——从机制上杜绝了"启动了一个永远跑不起来的处理器"。 - 静态文件服务与 WebSocket 升级仍然只接受
GET/HEAD。路由阶段 httplib.h 明确只在GET/HEAD时进入handle_file_request(),自定义方法不会绕开这一限制。
校验机制的底层实现
注册校验集中在 httplib.h 的custom_entry_for_registration():
if (!detail::fields::is_token(method) || builtin_methods().count(method)) { output_error_log(Error::InvalidHTTPMethod, nullptr); has_invalid_registration_ = true; return nullptr; }一旦校验失败:错误会被上报到错误日志器(可通过set_error_logger()观察,错误码为Error::InvalidHTTPMethod),并且has_invalid_registration_被置位。该标志是"粘性"的——test/test.cc 的RejectionIsSticky测试证明,即使先成功注册了PROPFIND,随后一次失败的GET注册也会让整个服务器实例失效。Server::is_valid()的实现就是直接返回!has_invalid_registration_(httplib.h),SSLServer::is_valid()则在 TLS 上下文之外叠加了这一判断(httplib.h)。
测试对注册规则的覆盖
仓库测试 test/test.cc 对注册规则做了全面验证:
RejectsBuiltInMethods:对十个内置方法逐一注册,断言is_valid()与listen()均失败;RejectsNonTokenMethods:对空串、"PRO PFIND"、"PROP\tFIND"、"PROP/FIND"、"PROP,FIND"、"PROP:FIND"、"PROP(FIND)"、含控制字符的方法名逐一注册,断言is_valid()失败;AcceptsWebDavAndUpnpMethods:PROPFIND、PROPPATCH、MKCOL、COPY、MOVE、LOCK、UNLOCK、REPORT、SUBSCRIBE全部注册成功,is_valid()为真;ReportsRejectionToErrorLogger:确认失败注册通过错误日志器上报Error::InvalidHTTPMethod。
无请求体的自定义方法:行为与边界
WebDAV 的MKCOL(创建集合)通常不带请求体。内置Delete()的 content-reader 重载早已支持无请求体请求,CustomRoute()保持了相同语义:普通 handler 形式在无请求体时正常工作(见 test/test.cc 的CustomRouteWithoutBody,返回201 Created);content-reader 形式同样会触发。此外 test/test.cc 的CustomMethodWithoutFraming测试确认:一个既无Content-Length也无Transfer-Encoding的裸PROPFIND请求会被立即应答(返回207 Multi-Status),而不会阻塞在等待 EOF 的读取上。
实战:一个最小的 WebDAV 轮廓
把以上要素串起来,一个可运行的 WebDAV 服务骨架如下:
#include "httplib.h" int main() { httplib::Server svr; // 1. 注册扩展方法(必须在 listen() 之前) svr.CustomRoute("PROPFIND", "/dav/:id", [](const httplib::Request &req, httplib::Response &res) { res.status = httplib::StatusCode::MultiStatus_207; res.set_content("<multistatus xmlns=\"DAV:\"/>", "application/xml"); }); svr.CustomRoute("MKCOL", "/dav/:id", [](const httplib::Request &req, httplib::Response &res) { res.status = httplib::StatusCode::Created_201; }); // 2. 用 OPTIONS 通告能力 svr.Options("/dav/.*", [](const httplib::Request &req, httplib::Response &res) { res.set_header("DAV", "1"); res.set_header("Allow", "OPTIONS, GET, HEAD, PROPFIND, MKCOL"); }); // 3. 注册失败时 listen() 会直接失败,先做防御性检查 if (!svr.is_valid()) { return 1; } svr.listen("0.0.0.0", 8080); }边界说明:协议实现仍在库外
cpp-httplib 的自定义方法能力止步于"把方法路由到你的处理器"。如果目标是完整实现 WebDAV,以下几点需要自己实现:
- 生成
207 Multi-Status的 XML 文档:<multistatus>、<response>、<propstat>等元素结构及DAV:命名空间; - 解析
Depth请求头:决定PROPFIND是仅查询当前资源还是递归子资源; - 锁管理(
LOCK/UNLOCK):lock-token、锁超时、锁冲突(423 Locked)等状态处理。
正如文档所述,协议本身在库之外,CustomRoute()只是给你一条接入它的路径。关于注册处理器的更多基础,可参考 S01. Register GET / POST / PUT / DELETE handlers(对应英文版 S01)。
- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
相关推荐
WebDAV协议扩展:如何实现自定义属性和方法的终极指南
WebDAV协议扩展:如何实现自定义属性和方法的终极指南 WebDAV协议作为HTTP的扩展,提供了强大的文件管理和协作功能。本文将深入探讨WebDAV协议的自
后端存储网络requests 怎么发送 MKCOL 等自定义 HTTP 动词?使用 request() 方法对接 WebDAV 服务
requests 怎么发送 MKCOL 等自定义 HTTP 动词?使用 request 方法对接 WebDAV 服务 对接 WebDAV 类服务时,经常会用到
后端网络通信RabbitMQ扩展协议:自定义协议支持
RabbitMQ扩展协议:自定义协议支持 在分布式系统中,消息队列(Message Queue)扮演着至关重要的角色,它能够解耦系统组件、提高系统的可扩展性和可
后端消息队列消息路由
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考