Chapter Orientation

The six chapters from here are TLS — the final confluence of Volume 8's primitives. This chapter covers the client: the side that initiates. xrtTlsDial hosts the whole chain "DNS resolution → racing TCP addresses → TLS handshake" in one call, handing you the encrypted stream (xtlsstream) on success; callback and Future consumption forms match event-driven and imperative code styles. The chapter walks the client state machine (HANDSHAKE → READY → CLOSED), the configuration trio (verifier/ServerName/ALPN), post-READY application data exchange and authenticated close — Chapter 84 then drills into the handshake's internal timing and key schedule, and Chapter 85 unfolds verifiers and policy. All prerequisites come from this volume: certificate verification (Chapters 80/81), AEAD and key derivation (Chapters 74/75), and the carrying TCP stream (Chapter 67).

82.1引入

How many steps does writing an HTTPS client string together? Resolve the hostname (Chapter 66's DNS), race-connect multiple addresses happy-eyeballs style (Chapter 63), complete the TLS handshake after TCP establishment (certificate verification + key exchange + mutual Finished), and only then send the request and receive the response — any step failing must clean up every resource already built. Hand-writing this orchestration is a classic handle-leak disaster zone; xrtTlsDial condenses it into one call: failure automatically rolls back intermediate resources, success transfers the stream reference to the completion callback, and the main thread handles only the final state.

The orchestration goes to the library; the decisions stay with you: which CA to trust (verifier, Chapter 85), which name to connect to (SNI auto-taken from the hostname), which protocol to speak (ALPN array), total timeout. This layering is the library-wide "hosted skeleton, injected policy" — Chapter 62's skeleton-industrialization idea reappearing at the TLS layer.

82.2概念

The client state machine and the dial chain

1SubmitxrtTlsDial(Engine, Resolver, hostname, port, TLS config, dial config, stream callbacks...)
2ResolveResolver asynchronously resolves the hostname (Chapter 66)
3Race-connectmultiple addresses tried in order for TCP (failure auto-switches to the next)
4HandshakeClientHello -> ... -> mutual Finished (diagrammed in Chapter 84)
5READYthe Stream reference transfers to the completion callback; application send/receive begins
6Final stateauthenticated close (close_notify) or failure -> release

The dial object xtlsdial itself is destroyable — xrtTlsDialDestroy does not affect the in-flight stream; the total timeout (DialConfig.Timeout) covers every stage. The failure-path contract: on dial or handshake failure no Stream callback is published — you never receive a half-open stream; cleanup is already done by the hosting layer.

The configuration trio

  • Verifier (TlsConfig.Verifier): a full-certificate handshake must bind one explicitly, otherwise creation fails — "no verification configured" is not an option but an error; this is the biggest attitudinal difference from many libraries. The verifier deep-copies the trust-store snapshot and is shareable across clients (Chapter 85 unfolds).
  • ServerName: SNI and the certificate verification name auto-taken from the hostname parameter — no manual filling in dial scenarios; with the low-level xrtTlsClientCreate entry directly, specify in xtlsclientconfig.ServerName.
  • ALPN (Protocols array): declare application protocols by preference (h2, http/1.1); the negotiated result is queryable after the handshake. A resumed connection's ALPN must match the ticket's binding.

After READY: application data and closing

The post-READY xtlsstream is an encrypted bidirectional stream whose API surface is isomorphic to Chapter 67's TCP stream, plus TLS-specific pieces: xrtTlsStreamSend (plaintext in, ciphertext out; iWritten reports the accepted amount), xrtTlsStreamAvailable/Buffer/Consume (zero-copy consumption of received plaintext — Span by span, the same vocabulary as Chapter 65's buffer chain), xrtTlsStreamClose (authenticated close: queue close_notify, await the peer's answer, drain, then enter CLOSED), xrtTlsStreamAbort (immediate abort, error paths only). On receiving the peer's close_notify the client automatically queues one answer, ignores subsequent data, and enters CLOSED after draining — the close protocol is executed by the state machine; you call Close once.

Both send-budget tiers (the TLS record layer's SendLimit and the TCP write budget, Chapter 68) can make Send return "partial acceptance" — continuing from the iWritten offset is the standard form (this chapter's exampleTlsDialSend is the template). KeyUpdate's automatic rotation (rekeying at record-usage thresholds) is transparent to the application; manual rotation uses xrtTlsClientKeyUpdate.

Two consumption forms: callback and Future

  • Callback form (xrtTlsDial + event table: the four callbacks Open/Read/Writable/Close): Open (fires at READY — start sending the request), Read (plaintext arrived), Writable (send budget recovered — continue the unfinished prefix), Close (the sole final state, xnetresult + structured error). The main form for event-driven services and tools.
  • Future form (xrtTlsDialAsync): returns an xfuture waitable via xrtFutureWaitFor on any thread; after XFUTURE_RESOLVED, xrtFutureValue takes the stream and xrtTlsStreamRef takes over the reference. The main form for imperative code and coroutine integration. The two forms share the same dial configuration and stream objects — consistent with the dual-form tradition of Chapter 66's DNS resolution and Chapter 68's dialing.

Session resumption: tickets and PSK

With XRT_FEATURE_TLS_CLIENT_RESUME enabled, the client derives the resumption master secret from the final transcript, derives an independent PSK per NewSessionTicket, and deep-copies the ticket (including the actual SNI/ALPN/verified leaf identity). ResumeLimit (default 4, max 64, 0 disables) bounds the retained count, the full queue evicting oldest; xrtTlsClientTakeResume transfers the session's held reference to the caller — the cross-connection cache is yours to manage. The next ClientHello carries the ticket + a normal key share: if the server accepts, the certificate stage is skipped (PSK+DHE, still forward-secret); if not, a safe fallback to the full handshake — xrtTlsClientResumed reports the actual choice. TLS 1.2 offers no resumption.

82.3示例

First complete program: callback-form HTTPS dial, full chain

The following program comes from examples/tls/dial/main.c — system trust store + hostname verification + hosted dial + authenticated close, the complete template at the bare kernel layer (real business should go straight to Chapter 98's xhttp):

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]

What just happened. (1) Verifier assembly: StoreSystem snapshot → TlsVerifierCreate (deep copy; the Store is freed right after) — three lines are the entirety of "HTTPS default trust", unpacked in Chapter 85. (2) Dial submission: TlsDial(Engine, Resolver, host, 443, TlsConfig, DialConfig, Events...) — one call chaining DNS/race-connect/handshake; DialConfig.Timeout = 15s covers all stages, the hostname entering SNI and the verification name automatically. (3) READY callback orchestration: in Open, send the minimal HTTP request — exampleTlsDialSend is the standard template for two-tier backpressure (partial acceptance recorded in the Sent offset; XTLS_AGAIN returns awaiting Writable); with the request queued, StreamClose authenticated-closes the write side and keeps receiving the response. (4) Zero-copy receive: in the Read callback, Available/Buffer/Front/Consume print Span by Span — never copying the whole response; Chapter 65's span-chain consumption reused as-is. (5) Final state and cleanup: the Close callback records the sole final state (an atomic flag lets the main thread exit safely); the Cleanup section destroys Dial/Stream/Resolver/Engine/Verifier in reverse dependency order — any step failing takes the same path, zero handle leaks.

Second complete program: Future-form dial and resource reclamation

The second program comes from examples/tls/dial_future/main.c, the same chain with Future consumption:

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]

What just happened. (1) xrtTlsDialAsync returns a Future immediately — the initiating site doesn't wait; any thread can xrtFutureWaitFor (16 seconds slightly wider than the dial's 15-second timeout — the waiting window must cover the dial window). (2) The success path takes over in three steps: WaitFor → XFUTURE_RESOLVED 检查 → xrtFutureValue 取流 + xrtTlsStreamRef 接引用, then FutureDestroy immediately — the Future's and stream's references complete their handover at this point. (3) Waiting for resource reclamation after Abort is this chapter's unique knowledge point: Abort requests immediate abort, but the background network resources are still releasing — spin waiting for XTLS_STREAM_CLOSED/FAILED before Destroy; this is "the junction of the stream object's and the Future's lifecycles" (Chapter 67's "wait CLOSED before destroy" rule holds verbatim on TLS streams). (4) Read side by side with dial: the verifier/Engine/Resolver assembly is identical — the two forms differ only in "how you get the stream".

82.4契约

  • Verification mandatory: a full-certificate handshake must bind a Verifier or creation fails; a pure-PSK resumption build (no verification backends) may omit it.
  • Dial atomicity: any stage failing automatically cleans intermediate resources; failure publishes no Stream; the total timeout covers all stages; the Dial may be destroyed first without affecting the in-flight stream.
  • State machine: HANDSHAKE → READY → CLOSED (or FAILED), forward-only; XTLS_AGAIN is merely "more input or output space needed", never an error.
  • Send semantics: Send reports partial acceptance via iWritten; the TLS record layer's SendLimit and the TCP write budget (Chapter 68) form two-tier backpressure; KeyUpdate automatic rotation is transparent.
  • Close semantics: Close queues close_notify and waits for the peer's answer to drain; on receiving the peer's close_notify, answer automatically and ignore subsequent data; error paths always Abort.
  • Resumption boundaries: ticket cache ResumeLimit 0..64 (default 4); TakeResume transfers the reference, the cross-connection cache belongs to the caller; the resumed ALPN must match the ticket's; 1.2 has no resumption; 0-RTT unimplemented.
  • Unimplemented surface: CertificateRequest, client-certificate authentication, 0-RTT, TLS 1.2 renegotiation — scenarios needing client certificates are currently unavailable.
  • Threading: callbacks execute on Engine Workers (Chapter 64); the Future form is waitable on any thread; the Verifier is shareable across sessions (Chapter 85).

Common Mistakes

Pitfall 1: no verifier configured (or a verifier with an empty trust store):Pitfall 1: no verifier configured (or a verifier with an empty trust store)

Symptoms: wanting to "connect first, worry later", skipping verification — xrtTlsClientCreate/dial fails outright; or a hand-built verifier with an empty store likewise fails at creation.

Cause: XRT's position is no-verification is not an option. Libraries offering "optional verification" contribute a steady crop of man-in-the-middle incidents yearly; here the default path is welded shut — either give a verifier, or say explicitly what you are doing (a PSK resumption build).

错误写法
xrtTlsClientConfigInit(&Config);
/* no Verifier: under the full-handshake build, creation fails outright */
Session = xrtTlsClientCreate(&Config, pPool);
正确写法
xx509store* pStore = xrtX509StoreSystem();       /* system trust */
xrtTlsVerifierConfigInit(&VerifierConfig);
VerifierConfig.Store = pStore;
pVerifier = xrtTlsVerifierCreate(&VerifierConfig);
xrtX509StoreFree(pStore);                        /* deep-copied — free yours */
xrtTlsClientConfigInit(&Config);
Config.Verifier = pVerifier;
Session = xrtTlsClientCreate(&Config, pPool);
xrtTlsVerifierRelease(pVerifier);                /* the session holds a reference; release your own */
Pitfall 2: Send judged by return value, ignoring iWritten:Pitfall 2: Send judged by return value, ignoring iWritten

Symptoms: large requests occasionally truncated — Send returned XTLS_OK so all was assumed sent, but the two-tier budget accepted only part.

Cause: both the TLS record layer and the TCP write budget can make one Send accept only a prefix — XTLS_OK means "call succeeded", not "all queued". Fully isomorphic to Chapter 68's write-budget semantics.

错误写法
if ( xrtTlsStreamSend(pStream, pRequest, iSize,
		&iWritten) == XTLS_OK ) {
	/* when iWritten < iSize, the back half is lost */
}
正确写法
while ( Sent < RequestSize ) {
	if ( xrtTlsStreamSend(pStream, pRequest + Sent,
			RequestSize - Sent, &iWritten) ==
			XTLS_AGAIN ) {
		return;   /* budget full: await the Writable callback to continue */
	}
	Sent += iWritten;
}
Pitfall 3: destroying the Engine before the stream's final state:Pitfall 3: destroying the Engine before the stream's final state

Symptoms: at program exit xrtNetEngineDestroy hangs, or stream callbacks execute on already-freed context.

Cause: the stream's callbacks run on Engine Workers; destroying the Engine before the stream reaches CLOSED/FAILED pulls the execution thread out from under the callbacks. Chapter 67's order "request close → wait CLOSED → Destroy → destroy Engine last" holds verbatim on TLS streams, plus waiting for background release to finish after Abort.

错误写法
xrtTlsStreamAbort(pStream);
xrtTlsStreamDestroy(pStream);
xrtNetEngineDestroy(pEngine);   /* the stream may still be winding down */
正确写法
xrtTlsStreamAbort(pStream);
while ( (xrtTlsStreamState(pStream) != XTLS_STREAM_CLOSED) &&
		(xrtTlsStreamState(pStream) != XTLS_STREAM_FAILED) ) {
	xrtThreadYield();
}
xrtTlsStreamDestroy(pStream);
xrtNetEngineDestroy(pEngine);

Programming Exercises

Basic: minimal HTTPS GET

Run dial <某真实站点> (some real site) and watch the output: request sent, response printed span by span, authenticated close. Set DialConfig.Timeout to 1 millisecond to verify the total-timeout path and error message. Acceptance: the normal path prints the full response and exits 0; the timeout path's error message carries stage information.

Advanced: ALPN negotiation probing

Configure TlsConfig.Protocols as {"h2", "http/1.1"}; in the Open callback query and print the negotiated protocol name. Then switch to only {"http/1.1"} and compare on the same site. Hint: the negotiated result is a session's public property; an ALPN mismatch during the handshake fails the handshake — record the difference between the two failure shapes.

Challenge: two connections with resumption

Enable XRT_FEATURE_TLS_CLIENT_RESUME: after the first connection ends, TakeResume extracts the ticket; the second dial hands the ticket to the configuration, and xrtTlsClientResumed confirms the PSK path was taken (no certificate messages). Compare the two handshakes' time and byte counts. Acceptance: the resumed connection really skips the certificate stage; a ticket ALPN inconsistent with the second configuration's failure explainable; all ticket memory eventually freed (Chapter 6's stats).

Summary

TopicQuick reference
Hosted dialTlsDial: DNS → race-connect → handshake in one; failure auto-cleans, publishes no half-open stream
Configuration trioVerifier (explicitly required) / ServerName (auto from hostname) / ALPN array
Dual formscallback (Events: Open/Read/Writable/Close) / Future (DialAsync + WaitFor)
Send semanticspartial acceptance via iWritten; TLS SendLimit + TCP write budget — two-tier backpressure
Receive consumptionAvailable/Buffer/Front/Consume zero-copy Span by span, Chapter 65's vocabulary
Close protocolClose authenticated (close_notify drained); Abort for error paths
Exit orderrequest close → wait CLOSED/FAILED → Destroy the stream → destroy Engine last
KeyUpdateautomatic rekey at record thresholds; manual via TlsClientKeyUpdate; AGAIN has no half-updated state
Session resumptionticket + PSK + key share; TakeResume transfers the reference; ALPN must match the ticket; 1.2 no resumption
Unimplementedclient certificates, 0-RTT, 1.2 renegotiation — those scenarios currently unavailable

📖 API references in this chapter:tls (201 entries)tls_verify (17 entries)tls_client (13 entries)net (247 entries)

Click for full signatures, parameter constraints, return values and error notes