Ориентация главы

Шесть глав начиная с этой — 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概念

Машина состояний клиента и цепочка дозвона

1ПодачаxrtTlsDial(Engine, Resolver, имя хоста, порт, конфигурация TLS, конфигурация дозвона, колбэки потока...)
2РазборResolver асинхронно разбирает имя хоста (глава 66)
3Соревновательное соединениенесколько адресов пробуются по порядку (неудача - автоматически следующий)
4РукопожатиеClientHello -> ... -> двусторонний Finished (разбор с диаграммами в главе 83)
5READYссылка Stream передаётся колбэку завершения, начинается приём-передача приложения
6Конечное состояниеаутентифицированное закрытие (close_notify) или неудача -> освобождение

Объект дозвона 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):

examples/tls/dial/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/dial/main.c -lws2_32 -liphlpapiusage: dial <host> [port]

Что только что произошло. (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:

examples/tls/dial_future/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/dial_future/main.c -lws2_32 -liphlpapiusage: dial_future <host> [port]

Что только что произошло. (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 — лишь «нужно больше входа или места под вывод», не ошибка.
  • Семантика отправки: частичный приём Send reports через iWritten; слой записей TLS SendLimit и бюджет записи TCP (глава 68) — противодавление двух уровней; автоматическая ротация KeyUpdate прозрачна.
  • Семантика закрытия: Close ставит close_notify в очередь и ждёт ответа-стекания партнёра; полученный close_notify автоматически отвечается, последующие данные игнорируются; путь ошибок — всегда Abort.
  • Границы восстановления: кеш билетов ResumeLimit 0..64 (умолчание 4); TakeResume переносит ссылку, межсоединённый кеш — дело вызывающего; ALPN восстановления обязан совпадать с билетом; в 1.2 восстановления нет; 0-RTT не реализован.
  • Нереализованное: CertificateRequest, аутентификация клиентским сертификатом, 0-RTT, повторный negociación TLS 1.2 — сцены с клиентским сертификатом сейчас недоступны.
  • Потоки: колбэки исполняются на Worker-е Engine (глава 64); Future-форма ожидаема в любом потоке; Verifier разделяем между сессиями (глава 85).

Типичные ошибки

Ловушка 1: без верификатора (или верификатор с пустым хранилищем):Ловушка 1: без верификатора (или верификатор с пустым хранилищем)

Симптом: хочется «сперва подключиться, потом разберёмся», проверка пропущена — 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);                /* сессия держит ссылку, свою можно отпустить */
Ловушка 2: Send — смотреть только возврат, не iWritten:Ловушка 2: Send — смотреть только возврат, не iWritten

Симптом: большой запрос изредка обрезан — 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;
}
Ловушка 3: уничтожить Engine, не дождавшись конечного состояния потока:Ловушка 3: уничтожить Engine, не дождавшись конечного состояния потока

Симптом: при выходе 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);

Практические задания

Базовое: минимальный HTTPS GET

Прогоните dial <某真实站点> (dial <некий реальный сайт>) и пронаблюдайте вывод: запрос отправлен, ответ напечатан по сегментам, аутентифицированное закрытие. Установите DialConfig.Timeout в 1 миллисекунду и проверьте путь общего тайм-аута и сообщение ошибки. Критерии приёмки: нормальный путь полностью принимает ответ и выходит с кодом 0; путь тайм-аута содержит фазовую информацию в сообщении.

Продвинутое: зонд negociación ALPN

Сконфигурируйте 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки