路由:静态表 + 动态 pattern + 方法槽
宿主不内置路由器——这是设计而不是缺失。本章把 demo-single 验证过、routebench 万级路由压测过的那套路由框架拆给你:两张表、一次编译、三种返回。
本章导读
17.1 引入
"路由要不要进宿主"是个真问题:进了,灵活的路径需求(正则、前缀、通配)就开始挤压配置系统;不进,每个应用要自己写一遍。XServer 的选择是提供零件、不提供整机:xrtMap(哈希表)管精确匹配,xrtPattern(批量预编译的模式匹配器)管参数化路径——20 行装配代码就能得到一个生产级路由器,而特殊需求(正则路由、版本前缀)随时可以自己长出来。
17.2 双表模式
static void RouteHTTP_Init(void)
{
G_StaticRouteTableHTTP = xrtMapCreate(sizeof(RouteInfoHTTP));
/* 静态表:精确 URI(xrtMap 哈希)*/
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET, Handle_API_List);
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_POST, Handle_API_Add);
/* 动态表:{id} 段捕获(注册后统一编译)*/
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_GET, Handle_API_Item);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_DELETE, Handle_API_Item_Delete);
/* 段内捕获也支持:/api/v1/file/{name}.txt */
AddDynamicRouteHTTP("/api/v1/file/{name}.txt",
XHTTP_METHOD_GET, Handle_API_File);
/* 动态路由必须在全部注册完成后编译一次 */
(void)RouteHTTP_Compile();
}
刚才发生了什么:静态表 O(1) 命中;动态表在 RouteHTTP_Compile 里经 xrtPatternCompileMany 批量编译成不可变匹配器——之后的匹配耗时只与路径长度相关,与路由条数无关。处理器签名是 demo 惯例:(XS_HttpReq*, const RouteParamHTTP* arrParam, uint32 iParamCount),arrParam 携带 {id} 捕获的视图。
17.3 方法槽与 405
- 每个 (URI, 方法) 是一个槽——同 URI 多方法分别注册;URI 与方法都重复时后注册覆盖旧回调并告警
- 方法掩码自由组合:
PUT | PATCH共槽;XHTTP_METHOD_CRUD(五方法)与XHTTP_METHOD_ANY(含扩展方法)是聚合宏(枚举参考) - URI 命中但方法不中 → 405 + Allow 头:Allow 由该 URI 的全部已注册方法聚合生成——第 2 章
HEAD /json → 405, Allow: GET的出处
GET | HEAD——你的 ReplyRaw 已经统一处理 HEAD 不发正文(第 15 章),所以多数处理器零改动即可正确支持 HEAD。17.4 匹配顺序与性能
性能数字(routebench 实测,3 轮 × 10000 请求均值):
| 路由规模 | XServer 时延 | 对照 |
|---|---|---|
| 10 条 | 42.0 µs | Go 42.2 |
| 10,000 条 | 59.1 µs(+41%) | PHP +492% / FastAPI +935% |
平坦的秘诀:静态表哈希 O(1);动态表批量编译后匹配 O(路径长度)——万级路由只比十级贵 41%,而对照栈在万级普遍退化一个量级。
17.5 契约
- 编译线:动态路由注册完必须 Compile 一次;之后路由表不可变(换代重建是热重载的职责)。
- 顺序线:静态 → 动态 → FALLBACK;同一 URI 静态优先于动态。
- 参数线:arrParam 是请求级视图,回调内用完即弃。
常见错误
- 症状
- 所有动态路由 404/落静态层,静态路由正常。
- 原因
- 动态表只在 Compile 后才成为可匹配的不可变 pattern——注册只是收集。demo 把 Compile 放在 RouteHTTP_Init 末尾并注释"必须"。
- 症状
- "/api/{a}/{b}/{c}" 式的万能路由互相抢流量,匹配到错误的处理器。
- 原因
- pattern 是精确分段的(不是前缀/正则);但把很多不同资源塞进同一模式会让语义漂移。资源不同就分不同的 pattern。
编程练习
实现 /api/v1/users 与 /api/v1/users/{id} 的完整 REST(GET 列表/POST 创建/GET 单项/PUT+PATCH 更新/DELETE 删除 + HEAD 列表)。
用 -v 观察若干"URI 命中方法不中"的请求,核对 Allow 头与你的注册完全一致。
写脚本生成 1000 条静态 + 1000 条动态路由(参考 tools/routebench 的生成器),测量启动编译耗时与请求时延。
小结
| 要点 | 结论 |
|---|---|
| 双表 | xrtMap 静态 O(1) + xrtPattern 动态编译后平坦匹配 |
| 方法槽 | (URI × 方法) 一槽;掩码可组合;CRUD/ANY 聚合宏 |
| 405 | URI 中方法不中 → 405 + Allow(注册方法并集) |
| 顺序 | 静态 → 动态 → FALLBACK → 静态层/404 |
| 实测 | 万路由 59.1µs(+41%),对照栈退化一个量级 |
- xrt 参考 · pattern — CompileMany/Lookup 完整签名与捕获视图
- xrt 参考 · regex — 需要正则路由时的 RegexSet 批量预编译