Ориентация главы
Шесть глав начиная с этой — TLS, конечная точка схождения примитивов тома 8. Эта глава о клиенте — стороне, инициирующей соединение. xrtTlsDial за один вызов ведёт всю цепочку «разбор DNS → соревновательное TCP-соединение → рукопожатие TLS» и по успеху отдаёт вам шифрованный поток (xtlsstream); две формы потребления — колбэки и Future — соответствуют двум стилям кода, событийному и императивному. Глава проходит машину состояний клиента (HANDSHAKE → READY → CLOSED), конфигурационную тройку (верификатор/ServerName/ALPN), приём-передачу данных приложения после READY и аутентифицированное закрытие; глава 84 заглянет внутрь рукопожатия — тайминг и расписание ключей, глава 85 развернёт верификаторы и политики. Все предпосылки — из этого тома: проверка сертификатов (главы 80/81), AEAD и деривация ключей (главы 74/75), несущий TCP-поток (глава 67).
82.1引入
Сколько шагов сцепляет HTTPS-клиент? Разбор имени хоста (DNS главы 66), соревновательное соединение по нескольким адресам в духе happy-eyeballs (глава 63), после установления TCP — рукопожатие TLS (проверка сертификатов + обмен ключами + двусторонний Finished), и лишь затем отправка запроса и приём ответа — при неудаче любого шага придётся вычистить все ресурсы, построенные ранее. Рукописная оркестрация этого — классическое поле утечек дескрипторов; xrtTlsDial сжимает её в один вызов: неудача автоматически откатывает промежуточные ресурсы, успех передаёт ссылку потока колбэку завершения, главный поток обрабатывает только конечное состояние.
Оркестрация у библиотеки, решения остаются у вас: какому CA верить (верификатор, глава 85), к какому имени подключаться (SNI берётся из имени хоста автоматически), какой протокол говорить (массив ALPN), каков общий тайм-аут. Это расслоение — фирменное «скелет под управлением, политика вливается» — индустриализованная мысль скелета главы 62, воспроизведённая на слое TLS.
82.2概念
Машина состояний клиента и цепочка дозвона
Объект дозвона xtlsdial сам по себе уничтожаем — xrtTlsDialDestroy не трогает поток, уже находящийся в полёте; общий тайм-аут (DialConfig.Timeout) покрывает все фазы. Контракт пути неудачи: при провале дозвона или рукопожатия колбэк Stream не публикуется — вы не получите полузакрытый поток; очистка уже выполнена управляемым слоем.
Конфигурационная тройка
- Верификатор (
TlsConfig.Verifier): рукопожатие с полными сертификатами обязано явно привязать его, иначе провал при создании — «без проверки» не опция, а ошибка; это самое большое расхождение позиции со многими библиотеками. Верификатор глубоко копирует снимок хранилища доверия и разделяется несколькими клиентами (разбор в главе 84). - ServerName: SNI и проверочное имя сертификата берутся автоматически из параметра имени хоста — в сцене дозвона вручную заполнять не надо; у нижнего входа
xrtTlsClientCreateзадаются вxtlsclientconfig.ServerName. - ALPN (массив
Protocols): протоколы приложения по приоритету (h2,http/1.1); результат negociación читаем после рукопожатия. ALPN восстанавливаемого соединения обязан совпадать с привязанным к билету.
После READY: данные приложения и закрытие
xtlsstream после READY — шифрованный двунаправленный поток; грань API изоморфна TCP-потоку главы 67 плюс специфические для TLS вещи: xrtTlsStreamSend (открытый текст на входе, шифртекст на выходе; iWritten reports принятый объём), xrtTlsStreamAvailable/Buffer/Consume (нуль-копийное потребление принятого открытого текста — Span за 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принимает ссылку. Главная форма императивного кода и интеграции с корутинами. Обе формы делят одну конфигурацию дозвона и один объект потока — та же двухформенная традиция, что у разбора DNS главы 66 и дозвона главы 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 — системное хранилище доверия + проверка имени хоста + управляемый дозвон + аутентифицированное закрытие; полный прототип голого слоя ядра (в реальном бизнесе берите xhttp главы 98):
#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; }
Что только что произошло. (1) Сборка верификатора: снимок StoreSystem → TlsVerifierCreate (глубокая копия; Store сразу освобождается) — три строки и есть весь «HTTPS default-trust»; разбор в главе 84. (2) Подача дозвона: TlsDial(Engine, Resolver, host, 443, TlsConfig, DialConfig, Events...) — один вызов сцепляет DNS/соревновательное соединение/рукопожатие; DialConfig.Timeout = 15s покрывает все фазы, имя хоста автоматически идёт в SNI и проверочное имя. (3) Оркестрация в колбэке READY: в Open отправляется минимальный HTTP-запрос — exampleTlsDialSend стандартный шаблон двухуровневого противодавления (частичный приём запоминает смещение Sent, XTLS_AGAIN — возврат в ожидание Writable); запрос исчерпан — StreamClose аутентифицированно закрывает пишущую сторону, приём ответа продолжается. (4) Нуль-копийный приём: в колбэке Read печать по Span через Available/Buffer/Front/Consume — без копирования целого ответа, сегментное потребление главы 65 переиспользовано как есть. (5) Конечное состояние и уборка: колбэк 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; }
Что только что произошло. (1) xrtTlsDialAsync возвращает Future и немедленно — место подачи не ждёт; любой поток может xrtFutureWaitFor (16 секунд чуть шире 15-секундного тайм-аута дозвона: окно ожидания обязано накрывать окно дозвона). (2) Три шага принятия на успешном пути: WaitFor → XFUTURE_RESOLVED 检查 → xrtFutureValue 取流 + xrtTlsStreamRef 接引用 (проверка, взятие потока, приём ссылки); сразу после — FutureDestroy: ссылки Future и потока переданы в этой точке. (3) Ожидание возврата ресурсов после Abort — эксклюзивное знание главы: Abort просит немедленный разрыв, но фоновые сетевые ресурсы ещё освобождаются — крутитесь в ожидании XTLS_STREAM_CLOSED/FAILED и лишь потом Destroy; это «стык жизненных циклов объекта потока и Future» (правило главы 67 «дождаться CLOSED прежде Destroy» дословно действует на TLS-потоке). (4) Прочтите рядом с dial: сборка верификатора/Engine/Resolver в точности та же — две формы различаются только «как получить поток».
82.4契约
- Принудительность проверки: рукопожатие с полными сертификатами обязано привязать Verifier, иначе провал на этапе создания; сборка чистого PSK-восстановления (без бэкендов проверки подписи) может опустить.
- Атомарность дозвона: неудача любой фазы автоматически вычищает промежуточные ресурсы; при неудаче Stream не публикуется; общий тайм-аут покрывает все фазы; Dial можно уничтожить раньше, не тронув поток в полёте.
- Машина состояний:
HANDSHAKE → READY → CLOSED(или FAILED) только вперёд;XTLS_AGAIN— лишь «нужно больше входа или места под вывод», не ошибка. - Семантика отправки: частичный приём
Sendreports черезiWritten; слой записей TLSSendLimitи бюджет записи TCP (глава 68) — противодавление двух уровней; автоматическая ротация KeyUpdate прозрачна. - Семантика закрытия:
Closeставит close_notify в очередь и ждёт ответа-стекания партнёра; полученный close_notify автоматически отвечается, последующие данные игнорируются; путь ошибок — всегдаAbort. - Границы восстановления: кеш билетов
ResumeLimit0..64 (умолчание 4);TakeResumeпереносит ссылку, межсоединённый кеш — дело вызывающего; ALPN восстановления обязан совпадать с билетом; в 1.2 восстановления нет; 0-RTT не реализован. - Нереализованное: CertificateRequest, аутентификация клиентским сертификатом, 0-RTT, повторный negociación TLS 1.2 — сцены с клиентским сертификатом сейчас недоступны.
- Потоки: колбэки исполняются на Worker-е Engine (глава 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 зависает, либо колбэк потока исполняется на освобождённом контексте.
Причина: колбэки потока бегут на Worker-е Engine; уничтожение Engine до CLOSED/FAILED потока — вынуть из-под колбэков исполнительный поток. Порядок главы 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 <某真实站点> (dial <некий реальный сайт>) и пронаблюдайте вывод: запрос отправлен, ответ напечатан по сегментам, аутентифицированное закрытие. Установите DialConfig.Timeout в 1 миллисекунду и проверьте путь общего тайм-аута и сообщение ошибки. Критерии приёмки: нормальный путь полностью принимает ответ и выходит с кодом 0; путь тайм-аута содержит фазовую информацию в сообщении.
Сконфигурируйте TlsConfig.Protocols как {"h2", "http/1.1"} и в колбэке Open запросите и напечатайте названный negociación протокол. Затем оставьте только {"http/1.1"} и сравните на том же сайте. Подсказка: результат negociación — публичный атрибут сессии; несовпадение 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, повторный negociación 1.2 — эти сцены сейчас недоступны |
📖 Справочники API в этой главе:tls (201 записей)tls_verify (17 записей)tls_client (13 записей)net (247 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки