本章导读
从本章起的六章是 TLS——卷八原语的最终汇合点。本章讲客户端:发起连接的那一方。xrtTlsDial 一次托管"DNS 解析 → 竞争 TCP 地址 → TLS 握手"全链路,成功后把加密流(xtlsstream)交给你;回调与 Future 两种消费形态对应事件驱动与命令式两种代码风格。章内把客户端状态机(HANDSHAKE → READY → CLOSED)、配置三件套(验证器/ServerName/ALPN)、READY 后的应用数据收发与认证关闭走完整——第 84 章再钻进握手内部的时序与密钥调度,第 85 章展开验证器与策略。前置依赖全部来自本卷:证书验证(80/81 章)、AEAD 与密钥派生(74/75 章)、承载的 TCP 流(66 章)。
82.1引入
写一个 HTTPS 客户端要串起多少步?解析主机名(第 66 章的 DNS)、按 happy-eyeballs 竞争连接多个地址(第 63 章)、TCP 建立后完成 TLS 握手(证书验证 + 密钥交换 + 双向 Finished)、然后才是发送请求接收响应——任何一步失败都要清理前面已建的全部资源。手写这套编排是经典的句柄泄漏重灾区;xrtTlsDial 把它收成一次调用:失败自动回滚中间资源,成功把流引用转移给完成回调,主线程只处理终态。
编排交给库,决策留在你手里:信任哪个 CA(验证器,第 85 章)、连哪个名字(SNI 自动取自主机名)、说什么协议(ALPN 数组)、总超时多少。这个分层是全库一贯的"骨架托管、策略注入"——第 62 章的骨架工业化思想在 TLS 层的再现。
82.2概念
客户端状态机与拨号链
拨号对象 xtlsdial 本身可销毁——xrtTlsDialDestroy 不影响已在途的流;总超时(DialConfig.Timeout)覆盖全部阶段。失败路径的契约:拨号或握手失败时不会发布 Stream 回调——你不会收到一个半开的流;清理已由托管层完成。
配置三件套
- 验证器(
TlsConfig.Verifier):完整证书握手必须显式绑定,否则创建时失败——"不配置验证"不是选项而是错误,这是与很多库最大的态度差异。验证器深复制信任库快照、可被多个客户端共享(第 85 章展开)。 - ServerName:SNI 与证书验证名自动取自主机名参数——拨号场景不用手填;直接用
xrtTlsClientCreate的低层入口时在xtlsclientconfig.ServerName指定。 - ALPN(
Protocols数组):按优先级声明应用协议(h2、http/1.1);协商结果在握手后可查。恢复连接的 ALPN 必须与票据绑定一致。
READY 之后:应用数据与关闭
READY 后的 xtlsstream 是一条加密的双向流,API 面与第 67 章 TCP 流同构,外加 TLS 特有件:xrtTlsStreamSend(明文进、密文出,iWritten 报告受理量)、xrtTlsStreamAvailable/Buffer/Consume(接收明文的零拷贝消费——Span 逐段取用,与第 65 章缓冲链同一套词汇)、xrtTlsStreamClose(认证关闭:排队 close_notify、等对端应答、排空后进 CLOSED)、xrtTlsStreamAbort(立即中止,错误路径专用)。收到对端 close_notify 时客户端自动排队一次应答、忽略随后数据、排空后进 CLOSED——关闭协议由状态机执行,你只调用一次 Close。
两级发送预算(TLS 记录层 SendLimit 与 TCP 写预算,第 68 章)都会让 Send 返回"部分受理"——从 iWritten 偏移继续是标准写法(本章示例的 exampleTlsDialSend 就是模板)。KeyUpdate 的自动轮换(记录用量临界点自动换钥)对应用透明;主动轮换用 xrtTlsClientKeyUpdate。
两种消费形态:回调与 Future
- 回调式(
xrtTlsDial+ 事件表:Open/Read/Writable/Close四个回调):Open(READY 即触发,开始发请求)、Read(明文到达)、Writable(发送预算恢复,续传未完前缀)、Close(唯一终态,xnetresult+ 结构化错误)。事件驱动服务与工具的主形态。 - Future 式(
xrtTlsDialAsync):返回xfuture,任意线程xrtFutureWaitFor等待;XFUTURE_RESOLVED后xrtFutureValue取流并xrtTlsStreamRef接管引用。命令式代码、协程集成的主形态。两种形态共享同一拨号配置与流对象——与第 66 章 DNS 解析、第 68 章拨号的双形态传统一致。
会话恢复:票据与 PSK
启用 XRT_FEATURE_TLS_CLIENT_RESUME 后,客户端从最终 transcript 派生恢复主秘密,按每张 NewSessionTicket 派生独立 PSK 并深拷贝票据(含实际 SNI/ALPN/已验证叶证书身份)。ResumeLimit(默认 4,最大 64,0 禁用)限制保存量,满队列淘汰最旧;xrtTlsClientTakeResume 把会话持有的引用转移给调用方——跨连接缓存归你管。下一次 ClientHello 携带票据 + 正常 key share:服务端接受则跳过证书阶段(PSK+DHE,仍前向安全),不接受则安全回退完整握手——xrtTlsClientResumed 报告实际选择。TLS 1.2 不提供恢复。
82.3示例
第一个完整程序:回调式 HTTPS 拨号全链路
下面的程序来自 examples/tls/dial/main.c——系统信任库 + 主机名验证 + 托管拨号 + 认证关闭,裸内核层的完整范本(真实业务建议直接用第 98 章的 xhttp):
#include "../common.h" typedef struct example_tls_dial { char Request[1024]; size_t RequestSize; size_t Sent; xtlsstream* Stream; xatomic32 Finished; xnetresult Result; bool Closing; } example_tls_dial; /* 输出回调借用的结构化错误。 */ static void exampleTlsDialError(cstr sPrefix, const xerror* pError) { fprintf( stderr, "%s: %s\n", sPrefix, pError != NULL ? xrtErrorMessage(pError) : "unknown error" ); } /* 发送尚未受理的请求前缀,背压解除后从精确偏移继续。 */ static void exampleTlsDialSend(xtlsstream* pStream, example_tls_dial* pExample) { while ( pExample->Sent < pExample->RequestSize ) { size_t iWritten = 0; xtlsresult Result = xrtTlsStreamSend( pStream, pExample->Request + pExample->Sent, pExample->RequestSize - pExample->Sent, &iWritten ); pExample->Sent += iWritten; if ( Result == XTLS_AGAIN ) { return; } if ( Result != XTLS_OK ) { (void)xrtTlsStreamAbort(pStream); return; } } /* 请求排队后认证关闭写侧,仍继续接收服务端响应。 */ if ( !pExample->Closing ) { pExample->Closing = true; if ( !xrtTlsStreamClose(pStream) ) { (void)xrtTlsStreamAbort(pStream); } } } /* TLS READY 后开始发送最小 HTTP/1.1 请求。 */ static void exampleTlsDialOpen(xtlsstream* pStream, ptr pData) { exampleTlsDialSend(pStream, (example_tls_dial*)pData); } /* TLS 与 TCP 两级发送预算恢复后继续未受理前缀。 */ static void exampleTlsDialWritable(xtlsstream* pStream, ptr pData) { exampleTlsDialSend(pStream, (example_tls_dial*)pData); } /* 逐 Span 输出并精确消费借用明文,不复制整份响应。 */ static void exampleTlsDialRead( xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData ) { (void)pBuffer; (void)pData; while ( xrtTlsStreamAvailable(pStream) != 0 ) { const xnetbuf* pPlain = xrtTlsStreamBuffer(pStream); xnetspan Span; if ( (pPlain == NULL) || !xrtNetBufFront(pPlain, &Span) ) { (void)xrtTlsStreamAbort(pStream); return; } if ( fwrite(Span.Data, 1u, Span.Size, stdout) != Span.Size ) { (void)xrtTlsStreamAbort(pStream); return; } if ( !xrtTlsStreamConsume(pStream, Span.Size) ) { (void)xrtTlsStreamAbort(pStream); return; } } } /* 记录安全流唯一终态,主线程据此开始释放共享对象。 */ static void exampleTlsDialClose( xtlsstream* pStream, xnetresult Result, const xerror* pError, ptr pData ) { example_tls_dial* pExample = (example_tls_dial*)pData; (void)pStream; pExample->Result = Result; if ( Result != XNET_RESULT_OK ) { exampleTlsDialError("TLS stream failed", pError); } xrtAtomic32Store(&pExample->Finished, 1, XMEMORY_RELEASE); } /* Dial 只在 TLS READY 后交付 Stream;失败不会发布 Stream 回调。 */ static void exampleTlsDialDone( xtlsdial* pDial, xnetresult Result, xtlsstream* pStream, const xerror* pError, ptr pData ) { example_tls_dial* pExample = (example_tls_dial*)pData; (void)pDial; if ( Result == XNET_RESULT_OK ) { pExample->Stream = pStream; return; } pExample->Result = Result; exampleTlsDialError("TLS dial failed", pError); xrtAtomic32Store(&pExample->Finished, 1, XMEMORY_RELEASE); } */ /* 使用系统信任库连接主机名,并展示受管 DNS、TCP 与 TLS 生命周期。 */ int main(int argc, char** argv) { cstr sHost; unsigned long iPort = 443u; example_tls_dial Example = {0}; xx509store* pStore = NULL; xtlsverifierconfig VerifierConfig; xtlsverifier* pVerifier = NULL; xtlsclientconfig TlsConfig; xtlsdialconfig DialConfig; xtlsstreamevents Events; xnetengineconfig EngineConfig; xnetengine* pEngine = NULL; xnetresolver* pResolver = NULL; xtlsdial* pDial = NULL; int iRequestSize; int iResult = 1; /* 构建门禁不隐式依赖外部网络,真实连接由调用方明确触发。 */ if ( argc < 2 ) { printf("usage: dial <host> [port]\n"); return 0; } sHost = argv[1]; /* 可选端口必须完整落在 uint16 范围内。 */ if ( argc >= 3 ) { char* pEnd = NULL; iPort = strtoul(argv[2], &pEnd, 10); if ( (pEnd == argv[2]) || (*pEnd != 0) || (iPort == 0) || (iPort > 65535u) ) { fprintf(stderr, "invalid port\n"); goto Cleanup; } } /* 请求内容固定有界,过长主机名在连接前失败。 */ iRequestSize = snprintf( Example.Request, sizeof(Example.Request), "GET / HTTP/1.1\r\nHost: %s\r\n" "Connection: close\r\nUser-Agent: xrt-tls-dial\r\n\r\n", sHost ); if ( (iRequestSize < 0) || ((size_t)iRequestSize >= sizeof(Example.Request)) ) { fprintf(stderr, "host name is too long\n"); goto Cleanup; } Example.RequestSize = (size_t)iRequestSize; /* 验证器创建时复制系统信任快照,Store 随后即可释放。 */ pStore = xrtX509StoreSystem(); if ( pStore == NULL ) { exampleTlsDialError("failed to load system trust store", xrtGetError()); goto Cleanup; } xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Store = pStore; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); if ( pVerifier == NULL ) { exampleTlsDialError("failed to create TLS verifier", xrtGetError()); goto Cleanup; } xrtX509StoreFree(pStore); pStore = NULL; /* Engine 与 Resolver 分别拥有网络 Worker 和阻塞名称解析工作池。 */ xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { exampleTlsDialError("failed to start network engine", xrtGetError()); goto Cleanup; } pResolver = xrtNetResolverCreate(NULL); if ( pResolver == NULL ) { exampleTlsDialError("failed to create resolver", xrtGetError()); goto Cleanup; } /* 主机名自动进入 SNI 和证书验证名称,总超时覆盖全部阶段。 */ memset(&Events, 0, sizeof(Events)); Events.Open = exampleTlsDialOpen; Events.Read = exampleTlsDialRead; Events.Writable = exampleTlsDialWritable; Events.Close = exampleTlsDialClose; xrtTlsClientConfigInit(&TlsConfig); TlsConfig.Verifier = pVerifier; xrtTlsDialConfigInit(&DialConfig); DialConfig.Timeout = 15000; pDial = xrtTlsDial( pEngine, pResolver, sHost, (uint16)iPort, &TlsConfig, &DialConfig, &Events, &Example, exampleTlsDialDone, &Example ); if ( pDial == NULL ) { exampleTlsDialError("failed to submit TLS dial", xrtGetError()); goto Cleanup; } /* Dial 总超时和 Stream 关闭超时保证等待最终收敛。 */ while ( xrtAtomic32Load( &Example.Finished, XMEMORY_ACQUIRE ) == 0 ) { xrtThreadYield(); } iResult = Example.Result == XNET_RESULT_OK ? 0 : 1; Cleanup: xrtTlsDialDestroy(pDial); xrtTlsStreamDestroy(Example.Stream); if ( (pResolver != NULL) && !xrtNetResolverDestroy(pResolver) ) { exampleTlsDialError("failed to destroy resolver", xrtGetError()); iResult = 1; } if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) ) { exampleTlsDialError("failed to destroy network engine", xrtGetError()); iResult = 1; } xrtTlsVerifierRelease(pVerifier); xrtX509StoreFree(pStore); return iResult; }
刚才发生了什么。 ① 验证器装配:StoreSystem 快照 → TlsVerifierCreate(深复制,Store 随即释放)——三行就是"HTTPS 默认信任"的全部,第 85 章拆开讲。② 拨号提交:TlsDial(Engine, Resolver, host, 443, TlsConfig, DialConfig, Events...) 一个调用串起 DNS/竞连/握手;DialConfig.Timeout = 15s 覆盖全阶段,主机名自动进 SNI 与验证名。③ READY 回调编排:Open 里发送最小 HTTP 请求——exampleTlsDialSend 是两级背压的标准模板(部分受理记 Sent 偏移,XTLS_AGAIN 即返回等 Writable);请求排完 StreamClose 认证关闭写侧、继续收响应。④ 接收零拷贝:Read 回调里 Available/Buffer/Front/Consume 逐 Span 打印——不复制整份响应,第 65 章的段链消费原样复用。⑤ 终态与清理:Close 回调记录唯一终态(原子标志让主线程安全退出);Cleanup 段按依赖逆序销毁 Dial/Stream/Resolver/Engine/Verifier——任何一步失败都走同一段,无句柄泄漏。
第二个完整程序:Future 式拨号与资源回收
第二个程序来自 examples/tls/dial_future/main.c,同样的链路换 Future 消费:
#include "../common.h" #include <math.h> static inline double exampleTimerLimit(int64 Timeout) { return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0; } static inline bool exampleTimerExpired(double Limit) { return xrtTimer() >= Limit; } static inline int64 exampleTimerRemaining(double Limit) { double Ms; if (Limit == INFINITY) return XRT_WAIT_FOREVER; Ms = ceil((Limit - xrtTimer()) * 1000.0); return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms; } /* 输出当前线程借用的结构化错误。 */ static void exampleTlsDialFutureError(cstr sPrefix, const xerror* pError) { fprintf( stderr, "%s: %s\n", sPrefix, pError != NULL ? xrtErrorMessage(pError) : "unknown error" ); } /* 等待主动中止的 TLS Stream 释放全部后台网络资源。 */ static bool exampleTlsDialFutureClosed(xtlsstream* pStream) { double Deadline = exampleTimerLimit(INT64_C(5000)); while ( (xrtTlsStreamState(pStream) != XTLS_STREAM_CLOSED) && (xrtTlsStreamState(pStream) != XTLS_STREAM_FAILED) ) { if ( exampleTimerExpired(Deadline) ) { return false; } xrtThreadYield(); } return true; } */ /* 使用系统信任库以 Future 连接一个 TLS 主机名。 */ int main(int argc, char** argv) { cstr sHost; unsigned long iPort = 443u; xx509store* pStore = NULL; xtlsverifierconfig VerifierConfig; xtlsverifier* pVerifier = NULL; xtlsclientconfig TlsConfig; xtlsdialconfig DialConfig; xnetengineconfig EngineConfig; xnetengine* pEngine = NULL; xnetresolver* pResolver = NULL; xfuture* pFuture = NULL; xtlsstream* pStream = NULL; int iResult = 1; if ( argc < 2 ) { printf("usage: dial_future <host> [port]\n"); return 0; } sHost = argv[1]; if ( argc >= 3 ) { char* pEnd = NULL; iPort = strtoul(argv[2], &pEnd, 10); if ( (pEnd == argv[2]) || (*pEnd != 0) || (iPort == 0) || (iPort > 65535u) ) { fprintf(stderr, "invalid port\n"); goto Cleanup; } } pStore = xrtX509StoreSystem(); if ( pStore == NULL ) { exampleTlsDialFutureError( "failed to load system trust store", xrtGetError() ); goto Cleanup; } xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Store = pStore; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); if ( pVerifier == NULL ) { exampleTlsDialFutureError( "failed to create TLS verifier", xrtGetError() ); goto Cleanup; } xrtX509StoreFree(pStore); pStore = NULL; xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { exampleTlsDialFutureError( "failed to start network engine", xrtGetError() ); goto Cleanup; } pResolver = xrtNetResolverCreate(NULL); if ( pResolver == NULL ) { exampleTlsDialFutureError( "failed to create resolver", xrtGetError() ); goto Cleanup; } xrtTlsClientConfigInit(&TlsConfig); TlsConfig.Verifier = pVerifier; xrtTlsDialConfigInit(&DialConfig); DialConfig.Timeout = INT64_C(15000); pFuture = xrtTlsDialAsync( pEngine, pResolver, sHost, (uint16)iPort, &TlsConfig, &DialConfig, NULL, NULL ); if ( pFuture == NULL ) { exampleTlsDialFutureError( "failed to submit TLS dial", xrtGetError() ); goto Cleanup; } if ( xrtFutureWaitFor( pFuture, INT64_C(16000) ) != XWAIT_OK ) { (void)xrtFutureCancel(pFuture); exampleTlsDialFutureError( "TLS dial wait failed", xrtGetError() ); goto Cleanup; } if ( xrtFutureState(pFuture) != XFUTURE_RESOLVED ) { exampleTlsDialFutureError( "TLS dial failed", xrtFutureError(pFuture) ); goto Cleanup; } pStream = xrtTlsStreamRef( (xtlsstream*)xrtFutureValue(pFuture) ); if ( pStream == NULL ) { exampleTlsDialFutureError( "failed to retain TLS stream", xrtGetError() ); goto Cleanup; } xrtFutureDestroy(pFuture); pFuture = NULL; printf("TLS stream opened for %s:%lu\n", sHost, iPort); iResult = 0; Cleanup: if ( pStream != NULL ) { (void)xrtTlsStreamAbort(pStream); if ( !exampleTlsDialFutureClosed(pStream) ) { fprintf(stderr, "TLS stream did not close\n"); iResult = 1; } xrtTlsStreamDestroy(pStream); } if ( pFuture != NULL ) { (void)xrtFutureCancel(pFuture); xrtFutureDestroy(pFuture); } if ( (pResolver != NULL) && !xrtNetResolverDestroy(pResolver) ) { exampleTlsDialFutureError( "failed to destroy resolver", xrtGetError() ); iResult = 1; } if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) ) { exampleTlsDialFutureError( "failed to destroy network engine", xrtGetError() ); iResult = 1; } xrtTlsVerifierRelease(pVerifier); xrtX509StoreFree(pStore); return iResult; }
刚才发生了什么。 ① xrtTlsDialAsync 返回 Future 立即返回——发起处不等待,任何线程可以 xrtFutureWaitFor(16 秒略宽于拨号 15 秒超时,等待窗口要盖住拨号窗口)。② 成功路径三步接管:WaitFor → XFUTURE_RESOLVED 检查 → xrtFutureValue 取流 + xrtTlsStreamRef 接引用,随后立即 FutureDestroy——Future 与流的引用在这一点交接完毕。③ Abort 后的资源回收等待是本章独有知识点:Abort 请求立即中止,但后台网络资源仍在释放——自旋等 XTLS_STREAM_CLOSED/FAILED 再 Destroy,这是"流对象与 Future 生命周期的交界点"(第 67 章"等 CLOSED 再销毁"的规则在 TLS 流上原样成立)。④ 与 dial 并排读:验证器/Engine/Resolver 的装配完全一致——两种形态只差"怎么拿到流"。
82.4契约
- 验证强制:完整证书握手必须绑定 Verifier,否则创建期失败;纯 PSK 恢复构建(无验签后端)可省略。
- 拨号原子性:任一阶段失败自动清理中间资源;失败不发布 Stream;总超时覆盖全部阶段;Dial 可先销毁不影响在途流。
- 状态机:
HANDSHAKE → READY → CLOSED(或 FAILED)只向前推进;XTLS_AGAIN只是"需要更多输入或输出空间",不是错误。 - 发送语义:
Send部分受理以iWritten报告;TLS 记录层SendLimit与 TCP 写预算(第 68 章)两级背压;KeyUpdate 自动轮换透明。 - 关闭语义:
Close排队 close_notify 并等对端应答排空;收到对端 close_notify 自动应答并忽略后续数据;错误路径一律Abort。 - 恢复边界:票据缓存
ResumeLimit0..64(默认 4);TakeResume转移引用、跨连接缓存归调用方;恢复 ALPN 必须与票据一致;1.2 无恢复;0-RTT 未实现。 - 未实现面:CertificateRequest、客户端证书认证、0-RTT、TLS 1.2 重新协商——需要客户端证书的场景当前不可用。
- 线程:回调在 Engine Worker 上执行(第 64 章);Future 式可在任意线程等待;Verifier 可多会话共享(第 85 章)。
常见错误
症状:想"先连上再说",跳过验证——xrtTlsClientCreate/拨号直接失败;或自建验证器配了空 store,同样创建期失败。
原因:XRT 的立场是不验证不是选项。开放"可选验证"的库每年贡献大量中间人事故;这里把缺省路径焊死——要么给出验证器,要么明说你在做什么(PSK 恢复构建)。
xrtTlsClientConfigInit(&Config); /* 不配 Verifier:完整握手构建下创建直接失败 */ Session = xrtTlsClientCreate(&Config, pPool);
xx509store* pStore = xrtX509StoreSystem(); /* 系统信任 */ xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Store = pStore; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); xrtX509StoreFree(pStore); /* 深复制后即可释放 */ xrtTlsClientConfigInit(&Config); Config.Verifier = pVerifier; Session = xrtTlsClientCreate(&Config, pPool); xrtTlsVerifierRelease(pVerifier); /* 会话持引用,自己的可放 */
症状:大请求偶发被截断——Send 返回 XTLS_OK 就认为全部发出,实际两级预算只受理了一部分。
原因:TLS 记录层与 TCP 写预算都可能让一次 Send 只受理前缀——XTLS_OK 的语义是"调用成功",不是"全部排队"。这与第 68 章写预算的语义完全同构。
if ( xrtTlsStreamSend(pStream, pRequest, iSize, &iWritten) == XTLS_OK ) { /* iWritten < iSize 时后半截丢了 */ }
while ( Sent < RequestSize ) { if ( xrtTlsStreamSend(pStream, pRequest + Sent, RequestSize - Sent, &iWritten) == XTLS_AGAIN ) { return; /* 预算满:等 Writable 回调续传 */ } Sent += iWritten; }
症状:程序退出时 xrtNetEngineDestroy 卡住,或流回调在已释放的上下文上执行。
原因:流的回调跑在 Engine Worker 上;流未到 CLOSED/FAILED 就销毁 Engine,等于抽走回调的执行线程。第 67 章"请求关闭 → 等 CLOSED → Destroy → 最后销毁 Engine"的顺序在 TLS 流上原样成立,外加 Abort 后要等后台释放完成。
xrtTlsStreamAbort(pStream); xrtTlsStreamDestroy(pStream); xrtNetEngineDestroy(pEngine); /* 流可能仍在收尾 */
xrtTlsStreamAbort(pStream); while ( (xrtTlsStreamState(pStream) != XTLS_STREAM_CLOSED) && (xrtTlsStreamState(pStream) != XTLS_STREAM_FAILED) ) { xrtThreadYield(); } xrtTlsStreamDestroy(pStream); xrtNetEngineDestroy(pEngine);
编程练习
跑通 dial <某真实站点>,观察输出:请求发出、响应逐段打印、认证关闭。把 DialConfig.Timeout 调到 1 毫秒验证总超时路径与错误消息。验收标准:正常路径完整收完响应后程序退出码 0;超时路径错误消息包含阶段信息。
给 TlsConfig.Protocols 配 {"h2", "http/1.1"},在 Open 回调里查询并打印协商出的协议名。再改成只提供 {"http/1.1"},对同一站点对比结果。提示:协商结果属于会话的公开属性;握手期 ALPN 不匹配会导致握手失败——记录两种失败形态的区别。
启用 XRT_FEATURE_TLS_CLIENT_RESUME:第一次连接结束后 TakeResume 取出票据,第二次拨号把票据交给配置,用 xrtTlsClientResumed 确认走了 PSK 路径(无证书消息)。对比两次握手的耗时与字节数。验收标准:恢复连接确实跳过证书阶段;票据 ALPN 与第二次配置不一致时能解释失败原因;票据内存最终全部释放(第 6 章统计)。
小结
| 知识点 | 速查 |
|---|---|
| 托管拨号 | TlsDial:DNS→竞连→握手一次完成;失败自动清理、不发布半开流 |
| 配置三件套 | Verifier(必须显式)/ ServerName(自动取主机名)/ ALPN 数组 |
| 双形态 | 回调式(Events:Open/Read/Writable/Close)/ Future 式(DialAsync+WaitFor) |
| 发送语义 | 部分受理看 iWritten;TLS SendLimit + TCP 写预算两级背压 |
| 接收消费 | Available/Buffer/Front/Consume 零拷贝逐 Span,与第 65 章同词汇 |
| 关闭协议 | Close 认证关闭(close_notify 排空);Abort 错误路径专用 |
| 退出顺序 | 请求关闭 → 等 CLOSED/FAILED → Destroy 流 → 最后销毁 Engine |
| KeyUpdate | 记录临界自动换钥;主动用 TlsClientKeyUpdate;AGAIN 无半更新态 |
| 会话恢复 | 票据+PSK+key share;TakeResume 转移引用;ALPN 须与票据一致;1.2 无恢复 |
| 未实现 | 客户端证书、0-RTT、1.2 重新协商——这些场景当前不可用 |
📖 本章涉及的 API 参考手册:tls(201 个条目)tls_verify(17 个条目)tls_client(13 个条目)net(247 个条目)
点击查看完整签名、参数约束、返回值与错误说明