本章导读
easy 层(第 98 章)折叠了什么?本章展开给你看。请求构建器(xhttprequest):方法/URL/Header/正文/认证的显式装配,Do 提交时冻结快照——调用方可立即修改或销毁原请求;调用层:xhttpcalloptions 的两类截止时间(总超时覆盖全链、空闲超时只看无进展时长)、取消令牌、响应体限额;诊断:xhttpcallresult.Info 的状态机与时间戳(单调时钟毫秒)——RequestWireBytes/ResponseWireBytes 累计整条重定向链;连接池:按 origin 分片(最多 32 片)、每片独立等待 FIFO 与空闲 LRU、全局原子配额——复用是性能的根。四个齿轮合起来就是"运行时"——easy 的每一行都在这上面跑。
99.1引入
三个场景逼你从 easy 下沉。场景一:要发 JSON POST 带 Authorization 头——构建器三行的事,easy 做不到(故意的)。场景二:批量请求同一服务——每个请求新建 TCP+TLS 握手的开销是吞吐天花板;连接池的复用把第二次起的成本降到零握手。场景三:慢源站——总超时管"整个请求最多等多久",但一个"每小时推一个字节"的源站在总超时内永远不完;空闲超时专治这种"连接活着但没进展"的拖拽。
第三个场景值得多想一步:为什么需要两种超时?总超时是资源上限(这个请求最多占我 30 秒);空闲超时是进展检测(传输在动就继续等,卡住 10 秒就放弃)。收到请求字节、消费响应字节都刷新空闲计时——"慢但在动"与"快但卡死"由此可分。xhttp 把两者做成了正交配置:Timeout 与 IdleTimeout 各自独立、零值继承 Client、XHTTP_CLIENT_TIMEOUT_NONE 显式关闭。
99.2概念
请求构建器与冻结语义
xrtHttpRequestCreate(方法, URL) 建立可修改的请求;SetHeader/SetBytes(固定字节正文,复制一次)/SetBody(引用正文——文件、生产者流)/SetAuth 族(认证)逐项装配。冻结语义:xrtHttpClientDo 返回成功前复制请求与值语义配置、保留正文引用——提交后原请求可立即修改或销毁(异步执行的独立性由快照保证)。Clone 的方法/URL/Header/Trailer 完全独立、正文只加引用——"改一份发一份"的模板成本是 O(差量)。
调用层:截止时间、取消与限额
两条限额的层次要分清:ResponseBodyLimit 作用在去掉 HTTP/1 分帧后、Content-Encoding 解码前的表示正文——网络响应、缓存命中、本地组合的 Range 响应同预算;解码后的明文另由 Decompress.MaxBody 限制——高压缩比内容绕不过内存预算(第 97 章"炸弹防御"的客户端版)。SSE 这类长连接选无界表示正文、靠 Parser 的结构化限额约束——"限什么"由内容结构决定,不是一刀切。
诊断快照:Info 的全部字段
xhttpcallresult.Info(完成回调携带,也可任意线程复制运行中快照):State(排队/执行/不可变终态——终态发布后所有字段冻结,迟到的取消改写不了已发布结果)、Phase(实际结束阶段保留)、毫秒时间戳(TransportReady/RequestSent/FirstByte/Headers——单调时钟,未到为零)、字节统计(RequestWireBytes/ResponseWireBytes 累计整条重定向链;ResponseBodyBytes 是最终交付正文——自动解压时记明文)、ReusedConnection(任一跳复用过)、Secure(当前/最终跳加密)。这套字段就是"慢在哪一跳"的体检表——第 137 章性能分析会回来用它。
连接池:origin 分片与公平等待
启用 http_client_pool 后按 origin(scheme+大小写不敏感 host+port+代理身份)管理连接:
- 分片:按 Worker 数建最多 32 个固定 origin 分片,每片独立索引/等待 FIFO/空闲 LRU/清扫 Timer——不为每条连接建 Timer;跨片只用原子配额协调,无 Client 级热锁。
- 公平:同片等待按"最早可运行"FIFO;连接归还时同 origin 等待者直接接管;全局额度耗尽时跨片轮转分发——没有"某片永远休眠"。
- 回池条件:消息边界完整、无
Connection: close、无升级、传输健康——四条全过才复用;不用 HTTP/1 pipelining(一条连接同时一个事务——第 90 章走私防御的工程兑现)。 - 默认值:活动不限、全局空闲 128、每 origin 空闲 8、90 秒清扫;空闲上限零=关闭复用。
- 运维口:
xrtHttpClientCloseIdle(只摘空闲不打断活动)、xrtHttpClientStats(可并发读的数量与单调计数)。
提交即验证、失败即冻结
Client 创建验静态配置;Do 提交验配置范围(地址回绕同步拒绝、不发布回调);终态一经发布不可改写——迟到的取消/超时/传输失败不会覆盖已发布结果。这三条把"配置错误"与"运行失败"都钉在明确的时点,调试时错误不再漂移。
99.3示例
第一个完整程序:构建器装配与 URL 内省
下面的程序来自 examples/http/client_request——JSON POST 的显式装配:
#include <stdio.h> #include <xhttp.h> /* 构建一条可交给 HTTP 客户端执行的 JSON 请求。 */ int main(void) { xhttprequest* pRequest = xrtHttpRequestCreate( XRT_STR_LITERAL("POST"), XRT_STR_LITERAL("https://api.example.test/items?dry_run=1") ); const xurl* pUrl; uint16 iPort; if ( (pRequest == NULL) || !xrtHttpRequestSetHeader( pRequest, XRT_STR_LITERAL("Accept"), XRT_STR_LITERAL("application/json") ) || !xrtHttpRequestSetBytes( pRequest, (xbytesview){ (cbytes)"{\"name\":\"xrt\"}", 14 }, XRT_STR_LITERAL("application/json; charset=utf-8") ) ) { xrtHttpRequestDestroy(pRequest); return 1; } pUrl = xrtHttpRequestUrl(pRequest); if ( !xrtUrlPort(pUrl, &iPort) ) { xrtHttpRequestDestroy(pRequest); return 1; } printf( "%.*s %.*s:%u\n", (int)xrtHttpRequestMethod(pRequest).Size, xrtHttpRequestMethod(pRequest).Data, (int)pUrl->Host.Size, pUrl->Host.Data, (unsigned)iPort ); xrtHttpRequestDestroy(pRequest); return 0; }
刚才发生了什么。 ① HttpRequestCreate(方法, URL) 一步建立请求——URL 是完整形态(scheme+host+query),内部由第 102 章的 xurl 解析。② SetHeader 加 Accept、SetBytes 复制 14 字节 JSON 正文并声明类型——构建器的两个常用 Set。③ xrtHttpRequestUrl 借出已解析的 URL 结构,xrtUrlPort 取数值端口(443——HTTPS 默认值由此补齐,第 102 章 XURL_PORT_VALUE 语义);打印证明构建器内省能力:方法、主机、端口都是可读事实。④ Destroy 收尾——请求不提交也能当"URL/方法的结构化持有者"用。真实提交:xrtHttpClientDo(pClient, pRequest, NULL, on_done, pCtx) 后立即 Destroy 原请求——冻结语义让你不必跟踪"异步还在用吗"。
第二个完整程序:连接池统计与空闲清理
第二个程序来自 examples/http/client_pool——运行时的运维面:
#include <stdio.h> #include <xhttp.h> /* 建立一个有界连接池,并读取初始并发统计。 */ int main(void) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xhttpclientstats Stats; xnetengine* pEngine; xhttpclient* pClient; xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { xrtNetEngineDestroy(pEngine); return 1; } xrtHttpClientConfigInit(&ClientConfig); ClientConfig.Pool.MaxConnections = 256; ClientConfig.Pool.MaxConnectionsPerOrigin = 32; ClientConfig.Pool.MaxWaiting = 1024; ClientConfig.Pool.MaxWaitingPerOrigin = 128; ClientConfig.Pool.MaxIdle = 64; ClientConfig.Pool.MaxIdlePerOrigin = 8; ClientConfig.Pool.IdleTimeout = INT64_C(30000); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { xrtNetEngineDestroy(pEngine); return 1; } if ( !xrtHttpClientStats(pClient, &Stats) ) { xrtHttpClientDestroy(pClient); xrtNetEngineDestroy(pEngine); return 1; } printf( "active=%zu idle=%zu waiting=%zu opened=%llu\n", Stats.ActiveConnections, Stats.IdleConnections, Stats.WaitingCalls, (unsigned long long)Stats.ConnectionsOpened ); (void)xrtHttpClientCloseIdle(pClient); xrtHttpClientDestroy(pClient); if ( !xrtNetEngineDestroy(pEngine) ) { return 1; } return 0; }
刚才发生了什么。 ① HttpClientStats(pClient, &Stats) 读取连接池当前量与单调生命周期计数——各字段不承诺同一全局时刻(并发读的松一致);ActiveConnections 含拨号/使用/保留配额的连接。② HttpClientCloseIdle 只摘空闲——不打断活动调用;生命周期终态还会等异步 Close 与分片 Timer 取消回调("从 LRU 摘除"≠"完全关闭"——两个时点的区分是这个 API 的诚实之处)。③ 配置面:xhttpclientpoolconfig 的空闲/等待/活动上限是部署参数——每 origin 8 条空闲对多数 API 客户端够用;上游连接数受限的服务调低全局上限。统计→调整→再统计是容量循环。
99.4契约
- 冻结语义:
Do成功返回前完成复制;提交后原请求可改可毁;Clone差量复制(正文加引用)。 - 两类截止:
Timeout提交起全链;IdleTimeout执行起无进展时长(收发刷新);零值继承 Client、TIMEOUT_NONE显式关;同刻到达稳定报 TOTAL。 - 取消:
CallCancel任意线程;提交前已取消不装 Timer 不启 DNS 直接终态;终态发布后不可改写。 - 双限额:
ResponseBodyLimit(表示正文、解码前、缓存同预算)+Decompress.MaxBody(明文)——高压缩比绕不过预算;SSE 用结构化限额替代下载限额。 - Info 冻结:终态字段冻结;时间单调时钟毫秒、未到为零;Wire 字段累计重定向链;Body 记最终明文。
- 池分片:≤32 片固定、片内 FIFO/LRU/Timer、跨片原子配额;回池四条件;无 pipelining;默认 128/8/90s。
- 池运维:CloseIdle 不打断活动;Stats 松一致可并发;Drain/Abort 走同一路径并等异步收尾。
- 提交验证:配置范围回绕同步拒绝不发布回调;创建期验 Dial/Stream/Exchange 静态配置。
- 裁剪:
HTTP_CLIENT_POOL/_FUTURE/_PREPARE/_STREAM独立——按用到的面裁剪。
常见错误
症状:Do 之后又 SetHeader——在途请求"没带上"新头,以为库有 bug。
原因:冻结语义——Do 返回成功那一刻快照已定,后续修改属于"下一次提交"。这是特性不是缺陷:否则每次提交都要等调用方宣布"改完了"。
pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx); xrtHttpRequestSetHeader(pReq, Extra, Value); /* 改不动在途的 */
pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx); xrtHttpRequestDestroy(pReq); /* 原请求使命结束 */ /* 追加需求?新建请求再提交一次——快照语义让并发互不影响 */
症状:总超时 30 秒、源站每 20 秒推一个字节——每个请求都"合法地"耗满 30 秒,吞吐被这类源站拖垮。
原因:总超时只看时长不看进展。空闲超时才是进展检测——"连接活着但没动"10 秒就该放弃。
xrtHttpClientConfigInit(&Config); Config.Timeout = 30000000ull; /* 只有总超时:一字节/20s 的源站拖满全程 */
xrtHttpClientConfigInit(&Config); Config.Timeout = 30000000ull; /* 总预算 30s */ Config.IdleTimeout = 10000000ull; /* 无进展 10s 即弃 */ /* 传输/收发都刷新空闲——"慢但在动"不受影响,"卡住"快速止损 */
症状:用 ActiveConnections + IdleConnections 与瞬时 QPS 做除法,结论漂移不定。
原因:Stats 的字段不承诺同一时刻——并发读取下各字段来自不同瞬间。容量决策要用单调生命周期计数(累计创建/销毁/复用次数)算趋势,而不是瞬时量算比例。
double load = (double)Stats.ActiveConnections / (Stats.ActiveConnections + Stats.IdleConnections); /* 瞬时混拍:漂移 */
/* 生命周期计数器单调:两次采样求速率/复用率——稳定可比较 */ double reuse_rate = (double)Stats.ReusedConnections / (double)(Stats.CreatedConnections + 1);
编程练习
用构建器发一条带 Authorization 头与 JSON 正文的 POST(对本地或 httpbin 服务),在完成回调打印状态与 Info.Phase。验收标准:提交后立即销毁原请求不影响结果;响应正文与 Header 完整可读。
起一个慢速服务(每秒推一字节)与一个卡死服务(接受连接不发数据),分别只设总超时、只设空闲、双设三种配置测量放弃时间。验收标准:三种配置的放弃时间与契约推导一致;能指出 Info.Phase 报告的结束阶段差异。
对同一 origin 连发 100 个请求(并发 8),从 Stats 的生命周期计数统计复用率与握手次数;再换 ClientConfig 关闭池(空闲上限零)对比。验收标准:复用率、总耗时、Info.ReusedConnection 命中率三组数据自洽;能解释关闭池后时间差来自哪里(TCP+TLS 握手 × 次数)。
小结
| 知识点 | 速查 |
|---|---|
| 构建器 | Create(方法,URL) + SetHeader/SetBytes/SetBody/SetAuth 族;Do 冻结快照 |
| 冻结语义 | 提交后原请求可改可毁;Clone 差量(正文加引用) |
| 两类截止 | Timeout 全链(提交起);IdleTimeout 无进展(收发刷新);零继承/NONE 关 |
| 取消 | 任意线程;提交前取消直接终态;终态冻结不可改写 |
| 双限额 | 表示正文(解码前,缓存同预算)+ 明文(解压后)——炸弹两道闸 |
| Info | 终态冻结;毫秒单调钟;Wire 累计重定向链;Body 记明文字节 |
| 池分片 | ≤32 固定片;片内 FIFO/LRU/Timer;跨片原子配额无热锁 |
| 回池条件 | 边界完整+无 close+无升级+健康;无 pipelining;默认 128/8/90s |
| 运维口 | CloseIdle 不打断活动;Stats 松一致——容量看单调计数 |
| 内省 | RequestUrl/Method 可读;UrlPort 补默认端口——请求也是结构化持有者 |
📖 本章涉及的 API 参考手册:xhttp-http_client(101 个条目)xhttp-http_client_runtime(40 个条目)net(247 个条目)
点击查看完整签名、参数约束、返回值与错误说明