第 17 章 / 共 51 章
卷三 · 协议开发

路由:静态表 + 动态 pattern + 方法槽

宿主不内置路由器——这是设计而不是缺失。本章把 demo-single 验证过、routebench 万级路由压测过的那套路由框架拆给你:两张表、一次编译、三种返回。

本章导读

站位:路由是 HTTP 业务的中枢。本章成品即 demo 的 modules/protocol.h——读完可以直接搬走。405+Allow 的真实行为在第 2 章实测过,这里讲清它的来龙去脉。

17.1 引入

"路由要不要进宿主"是个真问题:进了,灵活的路径需求(正则、前缀、通配)就开始挤压配置系统;不进,每个应用要自己写一遍。XServer 的选择是提供零件、不提供整机:xrtMap(哈希表)管精确匹配,xrtPattern(批量预编译的模式匹配器)管参数化路径——20 行装配代码就能得到一个生产级路由器,而特殊需求(正则路由、版本前缀)随时可以自己长出来。

17.2 双表模式

route.h —— 注册清单(真实源码)实机验证出处:demo-single/route.h
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:注册时用 GET | HEAD——你的 ReplyRaw 已经统一处理 HEAD 不发正文(第 15 章),所以多数处理器零改动即可正确支持 HEAD。

17.4 匹配顺序与性能

请求 → ① 静态表(xrtMap 精确命中 → 方法槽) 未中 → ② 动态 pattern(匹配 → 方法槽 → 捕获参数) 未中 → XS_FALLBACK → 静态层(wwwroot)/ 404

性能数字(routebench 实测,3 轮 × 10000 请求均值):

路由规模XServer 时延对照
10 条42.0 µsGo 42.2
10,000 条59.1 µs(+41%)PHP +492% / FastAPI +935%

平坦的秘诀:静态表哈希 O(1);动态表批量编译后匹配 O(路径长度)——万级路由只比十级贵 41%,而对照栈在万级普遍退化一个量级。

17.5 契约

  • 编译线:动态路由注册完必须 Compile 一次;之后路由表不可变(换代重建是热重载的职责)。
  • 顺序线:静态 → 动态 → FALLBACK;同一 URI 静态优先于动态。
  • 参数线:arrParam 是请求级视图,回调内用完即弃。

常见错误

忘记 RouteHTTP_Compile
症状
所有动态路由 404/落静态层,静态路由正常。
原因
动态表只在 Compile 后才成为可匹配的不可变 pattern——注册只是收集。demo 把 Compile 放在 RouteHTTP_Init 末尾并注释"必须"。
动态路由越写越宽
症状
"/api/{a}/{b}/{c}" 式的万能路由互相抢流量,匹配到错误的处理器。
原因
pattern 是精确分段的(不是前缀/正则);但把很多不同资源塞进同一模式会让语义漂移。资源不同就分不同的 pattern。

编程练习

基础REST 七槽

实现 /api/v1/users 与 /api/v1/users/{id} 的完整 REST(GET 列表/POST 创建/GET 单项/PUT+PATCH 更新/DELETE 删除 + HEAD 列表)。

验收:curl -I 列表返回 200(HEAD 走通);DELETE 列表返回 405 + Allow 列出五个方法。
进阶405 语义核对

用 -v 观察若干"URI 命中方法不中"的请求,核对 Allow 头与你的注册完全一致。

验收:Allow 集合 = 该 URI 全部注册方法的并集,无多无少。
挑战千路由生成器

写脚本生成 1000 条静态 + 1000 条动态路由(参考 tools/routebench 的生成器),测量启动编译耗时与请求时延。

验收:编译 < 100ms;时延与 20 条路由相比增幅 < 50%。

小结

要点结论
双表xrtMap 静态 O(1) + xrtPattern 动态编译后平坦匹配
方法槽(URI × 方法) 一槽;掩码可组合;CRUD/ANY 聚合宏
405URI 中方法不中 → 405 + Allow(注册方法并集)
顺序静态 → 动态 → FALLBACK → 静态层/404
实测万路由 59.1µs(+41%),对照栈退化一个量级
本章涉及的 API 参考
xrt 延伸阅读