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

Сессионная машина главы 86 сама не трогает сеть; клиентский дозвон главы 82 уже тайно пользовался слоем Stream. Эта глава разбирает слой Stream (xtlsstream) в лоб: серверная форма — в колбэке TCP Accept xrtTlsStreamAccept одним шагом строит комбинированный объект «TCP-поток + TLS-сессионная машина», после чего бизнес видит только чтение-запись открытого текста; событийная модель — четыре колбэка Open/Read/Writable/Close (та же, что у клиента главы 82); противодавление — приём открытого текста через Available + ожидание Writable при короткой записи: медленный клиент не разрывает память сервера; жизненный цикл — разделяемая конфигурация с подсчётом ссылок, единый выпуск после закрытия слушателя и всех соединений. Это очередное исполнение общебиблиотечного паттерна «комбинация скелетов»: Engine (глава 64) + Listener/Stream (глава 67) + TLS-сессионная машина (глава 86) слагаются в полный сервис, к которому можно подключиться напрямую openssl s_client.

87.1引入

Превратить спарринг-эхо главы 86 в настоящий сервис — не хватает лишь «транспортной разводки»: Accept даёт TCP-поток → строится комбинированный объект → рукопожатие бежит само → событийные колбэки разносят открытый текст. Звучит как тонкий слой, но каждая точка — место ловушки: кто кормит байты во время рукопожатия? Где останавливается потребление открытого текста, когда запись упёрлась в противодавление? Как согласовать закрытие соединения и закрытие слушателя, чтобы не возникло утечки памяти? Слой Stream стандартизует эту разводку — вы пишете бизнес-логику в колбэке Echo, жизненным циклом и противодавлением ведает комбинированный объект.

Форма сервиса тоже заслуживает взгляда: один процесс, один Engine, один Listener, N TLS-соединений разделяют одну и ту же конфигурацию сервера (идентичность/ALPN) и таблицу событий — подсчёт ссылок конфигурации делает «собрали раз, N соединений переиспользуют» без единой копии. Это продолжение одной линии с разделением идентичности главы 83 и верификатора главы 85: все тяжёлые объекты TLS — неизменяемые разделяемые значения.

87.2概念

Построение комбинированного объекта: стороны Accept и Dial

  • Сервер: xrtTlsStreamAccept(TCP流, 服务端配置, 流配置, 事件表, 用户数据, &流) — в колбэке TCP Accept одним шагом строит комбинированный объект; рукопожатие бежит само, по успеху срабатывает Open.
  • Клиент: xrtTlsDial/DialAsync (глава 82) внутри и есть «TCP-дозвон + StreamAttach в клиентском направлении» — у клиента нет отдельного образца Attach, потому что дозвон уже встроил его.

Обе стороны делят тип xtlsstream и весь прикладной интерфейс — Send/Available/Buffer/Consume/Close/Abort/State (шпаргалка главы 82). Различие только в способе построения и направлении.

Событийная модель и жёсткое противодавление

Четыре обратных вызова (колбэка) полностью изоморфны TCP-потоку (глава 67): Open (рукопожатие завершено/READY), Read (открытый текст прибыл), Writable (бюджет отправки восстановлен), Close (единственное конечное состояние: xnetresult + структурная ошибка). Механизм жёсткого противодавления: у зоны приёма открытого текста есть уровень воды (Available не ноль — есть необработанное); пока бизнес не потребил, приёмная сторона не продвигается — эхо-служба связывает «потребление открытого текста» и «отправку открытого текста» в один цикл: Span за Span Send; короткая запись (XTLS_AGAIN) — пауза потребления и ожидание Writable для продолжения. Окно TCP медленного клиента сжимается → записи TLS не уходят → потребление открытого текста на паузе → уровень приёма неподвижен → память постоянна. Это исполнение обещания «насос без блокировок» главы 62 на TLS.

Совместное использование конфигурации и жизненный цикл

1Сборкаидентичность (глава 83) + конфигурация сервера + конфигурация потока + таблица событий - всё на стеке или в разделяемых объектах
2Подключениена каждое TCP-соединение один StreamAccept, комбинированный объект держит ссылку конфигурации
3РаботаN соединений конкурентно, колбэки исполняются на Worker-е Engine (глава 64)
4ЗакрытиеClose-колбэк каждого соединения декрементирует счётчик; закрытие слушателя ставит флаг; оба в ноль - единый выпуск разделяемых объектов

Разделяемая конфигурация не освобождается до закрытия Listener и всех соединений — образец оркестрирует этот пункт сходки атомарными счётчиками (Connections/ListenerClosed). Правило одиночного соединения то же, что в главе 81: Close/Abort → дождаться XTLS_STREAM_CLOSED/FAILED → Destroy.

Три стыковочные детали, специфичные для TLS

Три стыковки реализации, о которых стоит знать: байты рукопожатия кормит комбинированный слой автоматически (вы ни разу не звали Feed — после StreamAccept рукопожатие «дело транспорта»); транспортный запрос xrtTlsStreamTransport(流) одалживает нижележащий TCP-поток — запросить адрес партнёра в Open (xrtNetStreamRemote) для журнала — стандартное движение; двухуровневое закрытие — StreamClose идёт со стеканием close_notify (глава 82), Close-колбэк срабатывает один раз на конечном состоянии комбинированного объекта. Конфигураций две: xtlsserverconfig (протокольная грань: идентичность/ALPN) и xtlsstreamconfig (грань потока: уровни воды буферов и пр.) — разделение забот протокола и транспорта доведено до конфигурационного слоя.

87.3示例

Первая полная программа: событийная TLS-служба эха

Программа ниже из examples/tls/stream/main.c — полный Echo на TLS 1.3, к которому напрямую подключается openssl s_client:

examples/tls/stream/main.c源码 ↗
#include "../common.h"























 */


/* Echo 服务共享配置只在 Listener 和全部连接关闭后释放。 */
typedef struct example_tls_stream {
	xtlsserverconfig ServerConfig;
	xtlsstreamconfig StreamConfig;
	xtlsstreamevents StreamEvents;
	xatomic32 Connections;
	xatomic32 ListenerClosed;
} example_tls_stream;



/* 输出当前线程保存的结构化根错误。 */
static void exampleTlsStreamError(cstr sPrefix)
{
	const xerror* pError = xrtGetError();

	fprintf(
		stderr,
		"%s: %s\n",
		sPrefix,
		pError != NULL ? xrtErrorMessage(pError) : "unknown error"
	);
}



/* 从借用明文块逐 Span 回送;短写等待 Writable 后继续。 */
static void exampleTlsStreamEcho(xtlsstream* pStream)
{
	while ( xrtTlsStreamAvailable(pStream) != 0 ) {
		const xnetbuf* pBuffer = xrtTlsStreamBuffer(pStream);
		xnetspan Span;
		size_t iWritten = 0;
		xtlsresult Result;

		if ( (pBuffer == NULL) || !xrtNetBufFront(pBuffer, &Span) ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
		Result = xrtTlsStreamSend(
			pStream,
			Span.Data,
			Span.Size,
			&iWritten
		);
		if ( (iWritten != 0) &&
			!xrtTlsStreamConsume(pStream, iWritten) ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
		if ( Result == XTLS_AGAIN ) {
			return;
		}
		if ( Result != XTLS_OK ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
	}
}



/* TLS READY 后记录活动连接。 */
static void exampleTlsStreamOpen(xtlsstream* pStream, ptr pData)
{
	example_tls_stream* pExample = (example_tls_stream*)pData;
	xnetaddr Remote;
	char Address[128];

	(void)xrtAtomic32FetchAdd(
		&pExample->Connections,
		1,
		XMEMORY_RELEASE
	);
	if ( xrtNetStreamRemote(
		xrtTlsStreamTransport(pStream),
		&Remote
	) && xrtNetAddrText(
		&Remote,
		Address,
		sizeof(Address)
	) ) {
		printf("TLS client open: %s\n", Address);
	}
}



/* 新明文和写背压解除都进入同一个无重复 Echo 路径。 */
static void exampleTlsStreamRead(
	xtlsstream* pStream,
	const xnetbuf* pBuffer,
	ptr pData
)
{
	(void)pBuffer;
	(void)pData;
	exampleTlsStreamEcho(pStream);
}



/* 写入队列重新可用时继续尚未消费的借用明文。 */
static void exampleTlsStreamWritable(xtlsstream* pStream, ptr pData)
{
	(void)pData;
	exampleTlsStreamEcho(pStream);
}



/* 记录组合连接终态;失败根因只在回调期间打印。 */
static void exampleTlsStreamClose(
	xtlsstream* pStream,
	xnetresult Result,
	const xerror* pError,
	ptr pData
)
{
	example_tls_stream* pExample = (example_tls_stream*)pData;

	(void)pStream;
	if ( (Result != XNET_RESULT_OK) && (pError != NULL) ) {
		fprintf(
			stderr,
			"TLS client closed: %s\n",
			xrtErrorMessage(pError)
		);
	}
	(void)xrtAtomic32FetchSub(
		&pExample->Connections,
		1,
		XMEMORY_RELEASE
	);
}



/* 在 TCP Accept 回调内一步建立服务端 TLS 组合对象。 */
static bool exampleTlsStreamAccept(
	xnetlistener* pListener,
	xnetstream* pTransport,
	ptr pData
)
{
	example_tls_stream* pExample = (example_tls_stream*)pData;
	xtlsstream* pStream = NULL;
	bool bAccepted;

	(void)pListener;
	bAccepted = xrtTlsStreamAccept(
		pTransport,
		&pExample->ServerConfig,
		&pExample->StreamConfig,
		&pExample->StreamEvents,
		pExample,
		&pStream
	);
	if ( bAccepted ) {
		xrtTlsStreamDestroy(pStream);
	}
	return bAccepted;
}



/* 记录 Listener 唯一关闭回调。 */
static void exampleTlsStreamListenerClose(
	xnetlistener* pListener,
	ptr pData
)
{
	example_tls_stream* pExample = (example_tls_stream*)pData;

	(void)pListener;
	xrtAtomic32Store(
		&pExample->ListenerClosed,
		1,
		XMEMORY_RELEASE
	);
}



/* 运行一个可由 openssl s_client 访问的 TLS 1.3 Echo 服务。 */
int main(int argc, char** argv)
{
	static const xstrview Protocols[] = {
		XRT_STR_INIT("http/1.1")
	};
	example_tls_stream Example;
	uint8* pCertificateData = NULL;
	uint8* pPrivateData = NULL;
	size_t iCertificateSize = 0;
	size_t iPrivateSize = 0;
	xbytesview Certificate;
	xtlsidentity* pIdentity = NULL;
	xnetengineconfig EngineConfig;
	xnetlistenconfig ListenConfig;
	xnetlistenerevents ListenerEvents;
	xnetengine* pEngine = NULL;
	xnetlistener* pListener = NULL;
	xnetaddr Address;
	unsigned long iPort = 8443u;
	int iResult = 1;

	if ( argc < 4 ) {
		printf(
			"usage: stream <rsa|p256|p384|ed25519> "
			"<certificate.der> <private.der> [port]\n"
		);
		return 0;
	}
	if ( argc >= 5 ) {
		char* pEnd = NULL;

		iPort = strtoul(argv[4], &pEnd, 10);
		if ( (pEnd == argv[4]) || (*pEnd != '\0') ||
			(iPort == 0) || (iPort > 65535u) ) {
			fprintf(stderr, "invalid port\n");
			goto Cleanup;
		}
	}
	if ( !exampleTlsReadFile(
		argv[2],
		&pCertificateData,
		&iCertificateSize
	) || !exampleTlsReadFile(
		argv[3],
		&pPrivateData,
		&iPrivateSize
	) ) {
		fprintf(stderr, "failed to read DER identity\n");
		goto Cleanup;
	}
	Certificate = (xbytesview) {
		pCertificateData,
		iCertificateSize
	};
	pIdentity = exampleTlsIdentity(
		argv[1],
		&Certificate,
		1u,
		(xbytesview) { pPrivateData, iPrivateSize }
	);
	if ( pIdentity == NULL ) {
		exampleTlsStreamError("failed to create TLS identity");
		goto Cleanup;
	}

	memset(&Example, 0, sizeof(Example));
	memset(&ListenerEvents, 0, sizeof(ListenerEvents));
	xrtTlsServerConfigInit(&Example.ServerConfig);
	Example.ServerConfig.Identity = pIdentity;
	Example.ServerConfig.Protocols = Protocols;
	Example.ServerConfig.ProtocolCount = 1u;
	Example.ServerConfig.RequireProtocol = true;
	xrtTlsStreamConfigInit(&Example.StreamConfig);
	Example.StreamEvents.Open = exampleTlsStreamOpen;
	Example.StreamEvents.Read = exampleTlsStreamRead;
	Example.StreamEvents.Writable = exampleTlsStreamWritable;
	Example.StreamEvents.Close = exampleTlsStreamClose;
	ListenerEvents.Accept = exampleTlsStreamAccept;
	ListenerEvents.Close = exampleTlsStreamListenerClose;

	xrtNetEngineConfigInit(&EngineConfig);
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		exampleTlsStreamError("failed to start network engine");
		goto Cleanup;
	}
	xrtNetListenConfigInit(&ListenConfig);
	if ( !xrtNetAddrAny(
		&ListenConfig.Address,
		XNET_FAMILY_IPV4,
		(uint16)iPort
	) ) {
		exampleTlsStreamError("failed to build listen address");
		goto Cleanup;
	}
	pListener = xrtNetListen(
		pEngine,
		&ListenConfig,
		&ListenerEvents,
		NULL,
		&Example
	);
	if ( (pListener == NULL) ||
		!xrtNetListenerLocal(pListener, &Address) ) {
		exampleTlsStreamError("failed to listen");
		goto Cleanup;
	}
	printf(
		"TLS echo listening on 0.0.0.0:%u; press Enter to stop\n",
		(unsigned)Address.Port
	);
	(void)getchar();
	if ( !xrtNetListenerClose(pListener) ) {
		exampleTlsStreamError("failed to close listener");
		goto Cleanup;
	}
	while ( (xrtAtomic32Load(
			&Example.ListenerClosed,
			XMEMORY_ACQUIRE
		) == 0) || (xrtAtomic32Load(
			&Example.Connections,
			XMEMORY_ACQUIRE
		) != 0) ) {
		xrtThreadYield();
	}
	iResult = 0;

Cleanup:
	xrtNetListenerDestroy(pListener);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) ) {
		exampleTlsStreamError("failed to destroy network engine");
		iResult = 1;
	}
	xrtTlsIdentityRelease(pIdentity);
	free(pPrivateData);
	free(pCertificateData);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/stream/main.c -lws2_32 -liphlpapiusage: stream <rsa|p256|p384|ed25519> ...

Что только что произошло. (1) Главный цикл Echo — эталон жёсткого противодавления: Available → Buffer → Front 取 Span → Send(部分受理 iWritten) → Consume(iWritten) → AGAIN 即返回 (доступно → буфер → Front берёт Span → Send с частичным приёмом iWritten → Consume(iWritten) → при AGAIN возврат) — и Read, и Writable входят в этот единственный путь без дублей; медленный клиент никогда не разорвёт память. (2) Разводка Accept: в колбэке TCP Accept StreamAccept одним шагом строит комбинированный объект — здесь же Destroy (демонстрация владения: успех Accept даёт ссылку, таблица событий приняла дальнейшее; настоящий сервис держит соединение, естественно живущее до закрытия). (3) Транспортный запрос в Open: StreamTransport одалживает TCP-поток, Remote даёт адрес, печатается «TLS client open: адрес» — мост между слоем TLS и транспортным слоем ровно один. (4) Сходка жизненных циклов: Close-колбэк декрементирует Connections, ListenerClose ставит флаг — главный поток ждёт обоих нулей и лишь затем освобождает разделяемую конфигурацию; вот закрытая сторона оркестровки «N соединений делят одну сборку». (5) Прочтите рядом с TCP-эхом главы 67: событийная модель, потребление по Span, продолжение после короткой записи — построчно изоморфны; добавились только рукопожатие и двухуровневое закрытие.

Вторая полная программа: полный тур возможностей слоя Stream

Вторая программа из examples/tls/stream_tour/main.c — пять строк покрывают продвинутую грань слоя Stream:

examples/tls/stream_tour/main.c源码 ↗



























 */

#include "../embedded_identity.h"

#include <stdio.h>
#include <string.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;
}

#define EXAMPLE_DEADLINE_MS	INT64_C(5000)

/* 客户端上下文:Open/Read 回调与主线程交接。 */
typedef struct example_tls_upgrade {
	volatile bool bOpen;
	volatile bool bClosed;
	volatile size_t Received;
	size_t iPullup;
	size_t iRead;
	size_t iConsume;
	char ReadOut[8];
	bool bSessionOk;
	bool bDataOk;
	bool bReadMore;
	bool bEventsSwapped;
	size_t iBound;
} example_tls_upgrade;

/* Worker 任务上下文:TCP 连接后升级为 TLS。 */
typedef struct example_upgrade_task {
	xnetstream* pTcp;
	xtlssession* pSession;  /* 仅 Attach 形态非空 */
	const xtlsclientconfig* pClient;
	const xtlsstreamconfig* pStream;
	const xtlsstreamevents* pEvents;
	ptr pData;
	xtlsstream* pTls;
	volatile bool bDone;
	bool bOk;
} example_upgrade_task;

/* 客户端事件集与热替换副本:文件级存储保证回调期间存活。 */
static xtlsstreamevents g_ClientEvents;
static xtlsstreamevents g_SwappedEvents;

static xtlsverifydecision exampleAcceptAll(
	const xtlspeer* pPeer, ptr pContext)
{
	(void)pPeer;
	(void)pContext;
	return XTLS_VERIFY_ACCEPT;
}

static bool exampleSpinUntil(volatile bool* pFlag)
{
	double iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);

	while ( !*pFlag ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}

/* ---- HTTP over TLS 段:ParseTls 双向解析。 ---- */
static volatile bool g_bHttpReqParsed;
static volatile bool g_bHttpRspParsed;
static volatile bool g_bHttpMode;

/* Worker 任务:热替换事件集 / 发送请求。 */
typedef struct example_http_task {
	xtlsstream* pStream;
	volatile bool bDone;
	bool bOk;
} example_http_task;

/* 请求与应答全文用 sizeof 定长——少一字节 Header 即不完整。 */
static const char arrRequest[] = "GET /x HTTP/1.1\r\nHost: h\r\n\r\n";
static const char arrResponse[] =
	"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi";

static void exampleHttpSendTask(xnetworker* pWorker, ptr pUserData)
{
	example_http_task* pTask = (example_http_task*)pUserData;
	size_t iWritten = 0;

	(void)pWorker;
	pTask->bOk = (xrtTlsStreamSend(pTask->pStream, arrRequest,
		sizeof(arrRequest) - 1u,
		&iWritten) == XTLS_OK) &&
		(iWritten == sizeof(arrRequest) - 1u);
	pTask->bDone = true;
}

/* Worker 任务:StreamClient(pSession == NULL)或 StreamAttach。 */
static void exampleUpgradeTask(xnetworker* pWorker, ptr pUserData)
{
	example_upgrade_task* pTask = (example_upgrade_task*)pUserData;

	(void)pWorker;
	if ( pTask->pSession != NULL ) {
		pTask->bOk = xrtTlsStreamAttach(pTask->pTcp,
			pTask->pSession, pTask->pStream, pTask->pEvents,
			pTask->pData, &pTask->pTls);
	}
	else {
		pTask->bOk = xrtTlsStreamClient(pTask->pTcp,
			pTask->pClient, pTask->pStream, pTask->pEvents,
			pTask->pData, &pTask->pTls);
	}
	pTask->bDone = true;
}

/* 服务端 Read 回调(Worker):从 TLS 明文块链解析请求并应答。 */
static void exampleHttpServerRead(xtlsstream* pStream,
	const xnetbuf* pBuffer, ptr pData)
{
	xhttp1head Head;
	xhttpfield Fields[8];
	xhttp1limits Limits;
	xhttp1errorinfo Error;
	size_t iWritten = 0;
	xnetspan Span;

	(void)pBuffer;
	(void)pData;
	
	 * 以 "GET" 起头的请求前缀。 */
	while ( xrtTlsStreamAvailable(pStream) >= 3u ) {
		if ( !xrtTlsStreamPullup(pStream, 3u, &Span) ||
			(Span.Size < 3u) ) {
			return;
		}
		if ( memcmp(Span.Data, "GET", 3u) == 0 ) {
			break;
		}
		if ( !xrtTlsStreamConsume(pStream, 1u) ) {
			return;
		}
	}
	xrtHttp1LimitsInit(&Limits);
	xrtHttp1HeadInit(&Head, Fields, 8);
	if ( (xrtHttp1RequestParseTls(pStream, &Head, &Limits,
			&Error) != XHTTP1_READY) ||
		(Head.Method.Size != 3u) ||
		(memcmp(Head.Method.Data, "GET", 3u) != 0) ||
		(Head.Target.Size != 2u) ||
		(memcmp(Head.Target.Data, "/x", 2u) != 0) ||
		(xrtTlsStreamSend(pStream, arrResponse,
			sizeof(arrResponse) - 1u,
			&iWritten) != XTLS_OK) ||
		(iWritten != sizeof(arrResponse) - 1u) ) {
		return;
	}
	g_bHttpReqParsed = true;
}

/* 客户端 Read 回调(Worker):从明文块链解析响应。 */
static void exampleHttpClientRead(xtlsstream* pStream,
	const xnetbuf* pBuffer, ptr pData)
{
	xhttp1head Head;
	xhttpfield Fields[8];
	xhttp1limits Limits;
	xhttp1errorinfo Error;

	(void)pBuffer;
	(void)pData;
	xrtHttp1LimitsInit(&Limits);
	xrtHttp1HeadInit(&Head, Fields, 8);
	if ( (xrtHttp1ResponseParseTls(pStream, &Head, &Limits,
			&Error) == XHTTP1_READY) &&
		(Head.Status == 200u) &&
		(Head.ContentLength == 2u) ) {
		g_bHttpRspParsed = true;
	}
}

/* 服务端回显:借用明文前缀逐块回送,短写等 Writable 再续。 */
static void exampleServerRead(xtlsstream* pStream,
	const xnetbuf* pBuffer, ptr pData)
{
	(void)pBuffer;
	(void)pData;
	if ( g_bHttpMode ) {
		exampleHttpServerRead(pStream, NULL, NULL);
		return;
	}
	while ( xrtTlsStreamAvailable(pStream) != 0 ) {
		const xnetbuf* pPlain = xrtTlsStreamBuffer(pStream);
		xnetspan Span;
		size_t iWritten = 0;

		if ( (pPlain == NULL) ||
			!xrtNetBufFront(pPlain, &Span) ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
		if ( xrtTlsStreamSend(pStream, Span.Data, Span.Size,
			&iWritten) != XTLS_OK ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
		if ( (iWritten != 0) &&
			!xrtTlsStreamConsume(pStream, iWritten) ) {
			(void)xrtTlsStreamAbort(pStream);
			return;
		}
	}
}

static void exampleUpgradeOpen(xtlsstream* pStream, ptr pData)
{
	example_tls_upgrade* pClient = (example_tls_upgrade*)pData;
	xnetspan Vec[2];
	size_t iWritten = 0;

	/* Open 回调(Worker 上):聚集发送一条 9 字节明文。 */
	Vec[0].Data = (cbytes)"vec-";
	Vec[0].Size = 4;
	Vec[1].Data = (cbytes)"gather";
	Vec[1].Size = 5;
	(void)xrtTlsStreamSendVec(pStream, Vec, 2, &iWritten);
	/* SendBound 是 Worker 专用:明文尺寸 → 密文上界。 */
	(void)xrtTlsStreamSendBound(pStream, 64u, &pClient->iBound);
	
	 * (先复制处理器再替换,语义等价但演示了热替换入口)。 */
	g_SwappedEvents = g_ClientEvents;
	pClient->bEventsSwapped = xrtTlsStreamSetEvents(pStream,
		&g_SwappedEvents, pClient);
	pClient->bSessionOk = xrtTlsStreamSession(pStream) != NULL;
	pClient->bDataOk = xrtTlsStreamData(pStream) == pClient;
	pClient->bOpen = true;
}

static void exampleUpgradeRead(xtlsstream* pStream,
	const xnetbuf* pBuffer, ptr pData)
{
	if ( g_bHttpMode ) {
		exampleHttpClientRead(pStream, NULL, NULL);
		return;
	}

	example_tls_upgrade* pClient = (example_tls_upgrade*)pData;
	size_t iAvail = xrtTlsStreamAvailable(pStream);
	size_t iWant = iAvail < 9u ? iAvail : 9u;
	xnetspan Span;

	(void)pBuffer;
	
	 * Pullup 按当前可用前缀请求连续化,而不是按期望总长。 */
	if ( (iWant != 0u) &&
		xrtTlsStreamPullup(pStream, iWant, &Span) &&
		(Span.Size == iWant) ) {
		pClient->iPullup = iWant;  /* 当前已连续化的前缀长度 */
	}
	/* 异步在途观测:Worker 内快照(主线程同样可读)。 */
	(void)xrtTlsStreamAsyncBytes(pStream);
	(void)xrtTlsStreamAsyncCount(pStream);
	if ( iAvail < 9u ) {
		/* 消息未到齐:ReadMore 在保留已到前缀的前提下请求继续累积。 */
		pClient->bReadMore = xrtTlsStreamReadMore(pStream);
		return;
	}
	
	 * 剩余 5 字节用 Consume 精确丢弃——Read 本身就是消费式复制。 */
	pClient->bReadMore = xrtTlsStreamReadMore(pStream);
	pClient->iRead = 0;
	(void)xrtTlsStreamRead(pStream, pClient->ReadOut, 4u,
		&pClient->iRead);
	if ( xrtTlsStreamConsume(pStream, 5u) ) {
		pClient->iConsume = 5u;
	}
	pClient->Received = pClient->Received + 9u;
}

static void exampleUpgradeClose(xtlsstream* pStream,
	xnetresult Result, const xerror* pError, ptr pData)
{
	example_tls_upgrade* pClient = (example_tls_upgrade*)pData;

	(void)pStream;
	(void)Result;
	(void)pError;
	pClient->bClosed = true;
}

/* 等一条 TCP Stream 进入 OPEN。 */
static bool exampleWaitTcpOpen(xnetstream* pTcp)
{
	double iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);

	while ( xrtNetStreamState(pTcp) != XNET_STREAM_OPEN ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}

int main(void)
{
	xnetengineconfig EngineConfig;
	xnetresolverconfig ResolverConfig;
	xtlslistenerconfig ListenerConfig;
	xtlsserverconfig ServerConfig;
	xtlsclientconfig ClientConfig;
	xtlsverifierconfig VerifierConfig;
	xtlsstreamconfig StreamConfig;

	xtlsstreamevents ServerEvents;
	xnetengine* pEngine = NULL;
	xnetresolver* pResolver = NULL;
	xtlsverifier* pVerifier = NULL;
	xtlsidentity* pIdentity = NULL;
	xtlslistener* pListener = NULL;
	xnetstream* pTcpA = NULL;
	xnetstream* pTcpB = NULL;
	xtlssession* pSessionB = NULL;
	xtlsstream* pClientA = NULL;
	xtlsstream* pClientB = NULL;
	xtlsstream* pServerA = NULL;
	xtlsstream* pServerB = NULL;
	example_tls_upgrade ClientA;
	example_tls_upgrade ClientB;
	example_upgrade_task TaskA;
	example_upgrade_task TaskB;
	xnetpost Post;
	xfuture* pSendFuture = NULL;
	xnetaddr Address;
	xnetspan AsyncVec[2];
	size_t iBound = 0;
	double iDeadline;
	int iResult = 1;

	memset(&ClientA, 0, sizeof(ClientA));
	memset(&ClientB, 0, sizeof(ClientB));

	/* 服务端:回显 Listener(Read 事件原样回送)。 */
	pIdentity = exampleEmbeddedIdentity();
	if ( pIdentity == NULL ) {
		fprintf(stderr, "failed to create embedded identity\n");
		goto Cleanup;
	}
	xrtTlsServerConfigInit(&ServerConfig);
	ServerConfig.Identity = pIdentity;
	xrtTlsListenerConfigInit(&ListenerConfig);
	ListenerConfig.Tls = ServerConfig;
	(void)xrtNetAddrLoopback(&ListenerConfig.Listen.Address,
		XNET_FAMILY_IPV4, 0);
	memset(&ServerEvents, 0, sizeof(ServerEvents));
	ServerEvents.Read = exampleServerRead;

	/* 客户端:接受一切 + 显式 VerifyName(直连不走 Dial 名字推导)。 */
	xrtTlsVerifierConfigInit(&VerifierConfig);
	VerifierConfig.Verify = exampleAcceptAll;
	pVerifier = xrtTlsVerifierCreate(&VerifierConfig);
	if ( pVerifier == NULL ) {
		fprintf(stderr, "failed to create verifier\n");
		goto Cleanup;
	}
	xrtTlsClientConfigInit(&ClientConfig);
	ClientConfig.Verifier = pVerifier;
	ClientConfig.VerifyName = XRT_STR_LITERAL("localhost");
	xrtTlsStreamConfigInit(&StreamConfig);
	memset(&g_ClientEvents, 0, sizeof(g_ClientEvents));
	g_ClientEvents.Open = exampleUpgradeOpen;
	g_ClientEvents.Read = exampleUpgradeRead;
	g_ClientEvents.Close = exampleUpgradeClose;

	xrtNetEngineConfigInit(&EngineConfig);
	EngineConfig.Workers = 1;
	xrtNetResolverConfigInit(&ResolverConfig);
	pEngine = xrtNetEngineCreate(&EngineConfig);
	pResolver = xrtNetResolverCreate(&ResolverConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ||
		(pResolver == NULL) ) {
		fprintf(stderr, "failed to start engine/resolver\n");
		goto Cleanup;
	}
	/* StreamEvents 让每条被接受的连接自动回显(队列中同样生效)。 */
	pListener = xrtTlsListenerStart(pEngine, &ListenerConfig, NULL,
		&ServerEvents, NULL);
	if ( (pListener == NULL) ||
		!xrtTlsListenerLocal(pListener, &Address) ) {
		iResult = 2;
		goto Cleanup;
	}

	/* 形态一(STARTTLS):裸 TCP → Worker 上 StreamClient 升级。 */
	pTcpA = xrtNetStreamConnect(pEngine, &Address, 0, NULL,
		NULL, NULL);
	if ( (pTcpA == NULL) || !exampleWaitTcpOpen(pTcpA) ) {
		iResult = 3;
		goto Cleanup;
	}
	memset(&TaskA, 0, sizeof(TaskA));
	TaskA.pTcp = pTcpA;
	TaskA.pClient = &ClientConfig;
	TaskA.pStream = &StreamConfig;
	TaskA.pEvents = &g_ClientEvents;
	TaskA.pData = &ClientA;
	if ( !xrtNetPostInit(&Post) || !xrtNetPost(
		xrtNetStreamWorker(pTcpA), &Post, exampleUpgradeTask,
		&TaskA) || !exampleSpinUntil(&TaskA.bDone) ||
		!TaskA.bOk ) {
		iResult = 3;
		goto Cleanup;
	}
	pClientA = TaskA.pTls;

	/* 形态二(Attach):显式会话 + 裸 TCP → Worker 上 Attach 组合。 */
	pSessionB = xrtTlsClientCreate(&ClientConfig, NULL);
	pTcpB = xrtNetStreamConnect(pEngine, &Address, 0, NULL,
		NULL, NULL);
	if ( (pSessionB == NULL) || (pTcpB == NULL) ||
		!exampleWaitTcpOpen(pTcpB) ) {
		iResult = 4;
		goto Cleanup;
	}
	memset(&TaskB, 0, sizeof(TaskB));
	TaskB.pTcp = pTcpB;
	TaskB.pSession = pSessionB;
	TaskB.pStream = &StreamConfig;
	TaskB.pEvents = &g_ClientEvents;
	TaskB.pData = &ClientB;
	if ( !xrtNetPostInit(&Post) || !xrtNetPost(
		xrtNetStreamWorker(pTcpB), &Post, exampleUpgradeTask,
		&TaskB) || !exampleSpinUntil(&TaskB.bDone) ||
		!TaskB.bOk ) {
		iResult = 4;
		goto Cleanup;
	}
	pClientB = TaskB.pTls;
	/* Attach 成功即接管调用方的 Session 引用;这里不能再 Destroy。 */
	pSessionB = NULL;

	/* 两个客户端都握手完成。 */
	if ( !exampleSpinUntil(&ClientA.bOpen) ||
		!exampleSpinUntil(&ClientB.bOpen) ) {
		iResult = 5;
		goto Cleanup;
	}
	printf("stream-tour: two clients upgraded, echo verified\n");

	/* 取出两条服务端流:队列中的连接被 Accept 消费后才开始驱动回显。 */
	{
		double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS);

		while ( (pServerA == NULL) || (pServerB == NULL) ) {
			xtlsstream* pOne = xrtTlsListenerAccept(pListener);

			if ( pOne != NULL ) {
				if ( pServerA == NULL ) {
					pServerA = pOne;
				}
				else {
					pServerB = pOne;
				}
				iEnd = exampleTimerLimit(
					EXAMPLE_DEADLINE_MS);
				continue;
			}
			if ( exampleTimerExpired(iEnd) ) {
				iResult = 6;
				goto Cleanup;
			}
			xrtThreadYield();
		}
	}

	/* 回显到达:Read 回调里完成 Pullup/Read/Consume 核对。 */
	iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);
	while ( ClientA.Received < 9u ) {
		if ( exampleTimerExpired(iDeadline) ) {
			iResult = 6;
			goto Cleanup;
		}
		xrtThreadYield();
	}
	if ( (ClientA.iPullup != 9u) || (ClientA.iRead != 4u) ||
		(ClientA.iConsume != 5u) ||
		(memcmp(ClientA.ReadOut, "vec-", 4) != 0) ||
		!ClientA.bSessionOk || !ClientA.bDataOk ||
		!ClientA.bReadMore || !ClientA.bEventsSwapped ||
		(ClientA.iBound < 64u) ) {
		iResult = 6;
		goto Cleanup;
	}
	printf("client(starttls): vec=9 bytes read=4 consume=%zu",
		ClientA.iConsume);
	printf(" session+data=%s\n",
		(ClientA.bSessionOk && ClientA.bDataOk) ? "ok" : "fail");

	/* Attach 客户端:SendVecAsync(Future 式聚集发送)。 */
	AsyncVec[0].Data = (cbytes)"asy-";
	AsyncVec[0].Size = 4;
	AsyncVec[1].Data = (cbytes)"ncvec";
	AsyncVec[1].Size = 5;
	pSendFuture = xrtTlsStreamSendVecAsync(pClientB, AsyncVec, 2);
	if ( (pSendFuture == NULL) ||
		(xrtFutureWaitFor(pSendFuture, EXAMPLE_DEADLINE_MS) !=
			XWAIT_OK) ||
		(xrtFutureState(pSendFuture) != XFUTURE_RESOLVED) ) {
		iResult = 7;
		goto Cleanup;
	}
	iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);
	while ( ClientB.Received < 9u ) {
		if ( exampleTimerExpired(iDeadline) ) {
			iResult = 7;
			goto Cleanup;
		}
		xrtThreadYield();
	}
	printf("client(attach): async-vec=ok future-written=9");

	/* Pending:发送队列排空。 */
	iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);
	while ( xrtTlsStreamPending(pClientB) != 0u ) {
		if ( exampleTimerExpired(iDeadline) ) {
			iResult = 8;
			goto Cleanup;
		}
		xrtThreadYield();
	}
	printf(" pending=0\n");

	/* SendBound 的结果已在 Open 回调(Worker 内)算好带回。 */
	iBound = ClientA.iBound;
	printf("bounds: 64 -> %zu ciphertext, error=%s\n", iBound,
		xrtTlsStreamError(pClientA) == NULL ? "(none)" : "err");
	(void)xrtTlsStreamAsyncBytes(pClientA);
	(void)xrtTlsStreamAsyncCount(pClientA);
	
	 * 双方 Read 回调内用 ParseTls 从明文块链解析。 ---- */
	g_bHttpMode = true;
	{
		example_http_task Send;

		memset(&Send, 0, sizeof(Send));
		Send.pStream = pClientB;
		if ( !xrtNetPost(xrtNetStreamWorker(pTcpB), &Post,
				exampleHttpSendTask, &Send) ||
			!exampleSpinUntil(&Send.bDone) || !Send.bOk ) {
			iResult = 9;
			goto Cleanup;
		}
	}
	iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS);
	while ( !g_bHttpReqParsed || !g_bHttpRspParsed ) {
		if ( exampleTimerExpired(iDeadline) ) {
			iResult = 9;
			goto Cleanup;
		}
		xrtThreadYield();
	}
	printf("http1-tls: request+response parsed on stream ok\n");
	iResult = 0;

Cleanup:
	xrtFutureDestroy(pSendFuture);
	
	 * 共享截止时间等终态,超时者 Abort 兜底。 */
	{
		xtlsstream* Streams[4];
		double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS);
		bool bSettled;

		Streams[0] = pClientA;
		Streams[1] = pClientB;
		Streams[2] = pServerA;
		Streams[3] = pServerB;
		for ( size_t i = 0; i < 4; ++i ) {
			if ( Streams[i] != NULL ) {
				(void)xrtTlsStreamClose(Streams[i]);
			}
		}
		do {
			bSettled = true;
			for ( size_t i = 0; i < 4; ++i ) {
				xtlsstreamstate State = Streams[i] != NULL ?
					xrtTlsStreamState(Streams[i]) :
					XTLS_STREAM_CLOSED;

				if ( (State != XTLS_STREAM_CLOSED) &&
					(State != XTLS_STREAM_FAILED) ) {
					bSettled = false;
				}
			}
			if ( bSettled || exampleTimerExpired(iEnd) ) {
				break;
			}
			xrtThreadYield();
		} while ( 1 );
		for ( size_t i = 0; i < 4; ++i ) {
			if ( Streams[i] != NULL ) {
				xtlsstreamstate State =
					xrtTlsStreamState(Streams[i]);

				if ( (State != XTLS_STREAM_CLOSED) &&
					(State != XTLS_STREAM_FAILED) ) {
					(void)xrtTlsStreamAbort(Streams[i]);
				}
			}
		}
		(void)exampleSpinUntil(&ClientA.bClosed);
		(void)exampleSpinUntil(&ClientB.bClosed);
	}
	xrtTlsStreamDestroy(pClientA);
	xrtTlsStreamDestroy(pClientB);
	xrtTlsStreamDestroy(pServerA);
	xrtTlsStreamDestroy(pServerB);
	xrtTlsSessionDestroy(pSessionB);
	if ( pTcpA != NULL ) {
		(void)xrtNetStreamAbort(pTcpA);
	}
	if ( pTcpB != NULL ) {
		(void)xrtNetStreamAbort(pTcpB);
	}
	xrtNetStreamDestroy(pTcpA);
	xrtNetStreamDestroy(pTcpB);
	if ( pListener != NULL ) {
		double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS);

		(void)xrtTlsListenerClose(pListener);
		while ( xrtTlsListenerState(pListener) !=
			XTLS_LISTENER_CLOSED ) {
			if ( exampleTimerExpired(iEnd) ) {
				break;
			}
			xrtThreadYield();
		}
	}
	xrtTlsListenerDestroy(pListener);
	xrtNetResolverDestroy(pResolver);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) &&
		(iResult == 0) ) {
		iResult = 10;
	}
	xrtTlsVerifierRelease(pVerifier);
	xrtTlsIdentityRelease(pIdentity);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/stream_tour/main.c -lws2_32 -liphlpapistream-tour: two clients upgraded, echo verifiedclient(starttls): vec=9 bytes read=4 consume=5 session+data=okclient(attach): async-vec=ok future-written=9 pending=0bounds: 64 -> 86 ciphertext, error=(none)http1-tls: request+response parsed on stream ok

Что только что произошло. (1) Строка 1 проверяет разом два клиентских пути: starttls (переговорное обновление на открытом соединении — официальный ответ на упражнение главы 87) и attach (прямая навеска TCP-потока) — вместе с серверным Accept полное множество трёх способов построения слоя Stream. (2) Строка 2 демонстрирует пакетную отправку Vec (9 байтов) и потребление Span (прочитано 4, потреблено 5 — потребление может обгонять границу чтения, семантика уровня воды). (3) Строка 3 идёт асинхронной записью Vec: после завершения Future Pending в ноль — облик Vec/асинхронной ступени пяти ступеней отправки главы 68 на TLS-потоке. (4) Строка 4 меряет границу шифртекста: 64 байта открытого текста → 86 байтов шифртекста (16-байтовая метка AEAD + накладные расходы заголовка записи) — реальный якорь для планирования ёмкости. (5) Строка 5 гонит на потоке полный разбор запроса-ответа HTTP/1 — самопроверка, сцепляющая примитивы глав 84–87 в одну цепь, и анонс тома 9.

87.4契约

  • Построение: сервер StreamAccept (один шаг внутри колбэка Accept); клиент через встроенный attach TlsDial/DialAsync; обе стороны делят весь прикладной интерфейс xtlsstream.
  • События: Open/Read/Writable/Close изоморфны TCP-потоку; колбэки исполняются на Worker-е Engine; Close — единственное уведомление о конечном состоянии (результат + структурная ошибка).
  • Жёсткое противодавление: остановка потребления = остановка приёма — возврат Span за Span + ожидание Writable при короткой записи есть стандартная эхо-форма; память отвязана от самого медленного клиента.
  • Транспортный мост: StreamTransport одалживает нижележащий TCP-поток (только-читающие запросы: адрес/состояние); кешировать между колбэками нельзя.
  • Двухуровневое закрытие: Close идёт со стеканием close_notify; Abort рвёт немедленно; одиночное соединение ждёт CLOSED/FAILED прежде Destroy; сервисный уровень ждёт «все соединения + слушатель» в ноль прежде выпуска разделяемой конфигурации.
  • Разделение конфигураций: xtlsserverconfig (протокольная грань) и xtlsstreamconfig (грань потока) независимы; конфигурационные объекты разделяемы подсчётом ссылок.
  • Автоматизация рукопожатия: цикл Feed/Drive исполняет комбинированный слой — приложение никогда не видит шифртекста (граничное измерение stream_tour — диагностическое употребление).
  • Обрезка: XRT_MODULE_TLS_STREAM независимо (зависит от NET) — нужна лишь сессионная машина без транспортной комбинации, обрезайте.

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

Ловушка 1: скопировать открытый текст в Read-колбэке и обрабатывать потом:Ловушка 1: скопировать открытый текст в Read-колбэке и обрабатывать потом

Симптом: пики памяти на высокой пропускной способности — Read-колбэк каждого соединения копирует всю зону приёма в собственную очередь, противодавление повисает в воздухе.

Причина: противодавление слоя Stream построено на «потреблении открытого текста (Consume)»; скопировать — не потребить, приёмная сторона продолжает продвигаться, и ваша очередь становится неограниченным буфером.

错误写法
static void onRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) {
	queue_push(g_MyQueue, pBuffer);   /* скопировали, но не Consume: противодавление обнулено */
}
正确写法
static void onRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) {
	/* сколько можем обработать - столько потребляем; остальное оставляем следующему Read/Writable */
	while ( xrtTlsStreamAvailable(pStream) != 0 ) {
		/* ... Front берёт Span, обработка + Consume ... */
		if ( 下游满了 ) { return; }   /* остановили потребление = остановили приём = противодавление работает */
	}
}
Ловушка 2: слушатель закрыт — и немедленно освобождена разделяемая конфигурация:Ловушка 2: слушатель закрыт — и немедленно освобождена разделяемая конфигурация

Симптом: в колбэке последнего соединения доступ к уже освобождённой идентичности/конфигурации — падение ровно на отключении последнего клиента.

Причина: подсчёт ссылок разделяемой конфигурации живёт на каждом соединении — закрытие Listener не равно закрытию соединений; выпуск обязан ждать обоих нулей.

错误写法
listenerClose(...) {
	xrtTlsIdentityRelease(g_Identity);  /* ещё есть активные соединения-пользователи */
}
正确写法
/* атомарная сходка счётчиков: Connections==0 && ListenerClosed - только тогда единый выпуск */
if ( (xrtAtomic32Load(&Connections, XMEMORY_ACQUIRE) == 0) &&
		(xrtAtomic32Load(&ListenerClosed, XMEMORY_ACQUIRE) != 0) ) {
	release_shared_assembly();
}
/* эту проверку делают оба колбэка - Close и ListenerClose */
Ловушка 3: TCP-поток, одолженный StreamTransport, сохранён на долгое пользование:Ловушка 3: TCP-поток, одолженный StreamTransport, сохранён на долгое пользование

Симптом: изредка перепутанное поведение — закешированный указатель TCP-потока рассогласовывается после продвижения внутреннего состояния комбинированного объекта.

Причина: StreamTransport — диагностическое/запросное представление «транспортного моста на данный момент», не долгий дескриптор. Нужна транспортная способность (запросить адрес, состояние) — берите в момент надобности и пользуйтесь тут же.

错误写法
g_SavedTcp = xrtTlsStreamTransport(pStream);   /* сохранить */
/* несколько колбэков спустя */
send_on_tcp(g_SavedTcp, ...);   /* прямая запись в транспорт мимо слоя TLS: рассогласование состояний */
正确写法
/* запрашивать по надобности (скажем, журнал в Open); отправка - всегда через Send слоя TLS */
if ( xrtNetStreamRemote(xrtTlsStreamTransport(pStream), &Remote) ) {
	log_client(Remote);
}
xrtTlsStreamSend(pStream, Data, Size, &Written);  /* открытый текст через парадную дверь */

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

Базовое: совместная отладка с openssl

Сгенерируйте самоподписанный сертификат, прогоните службу stream и подключитесь openssl s_client -connect localhost:8443 -alpn http/1.1; вводите текст и проверяйте эхо; пронаблюдайте адрес клиента, печатаемый сервером. Критерии приёмки: эхо побайтно верно; после разрыва (Ctrl-C или Q) срабатывает Close-колбэк сервера, счётчик соединений в ноль, процесс не выходит (продолжает обслуживать следующее соединение).

Продвинутое: прокси открытого текста (наблюдение со стороны расшифровки)

Вставьте перед Echo слой «аудита»: в Read-колбэке посчитайте байты открытого текста и число Span каждого соединения, на Close выдайте сводку (всего байтов / средний размер Span / длительность соединения). Критерии приёмки: 4 конкурентных соединения ведут статистику независимо; данные сходятся с отправленным openssl-ом; статистика не копирует открытый текст (по пути нулевого копирования).

Вызов: шлюз обновления StartTLS

Реализуйте шлюз «сперва команда открытого рукопожатия, потом обновление»: после TCP Accept не сразу TLS, а построчный разбор команд (строчное кадрирование главы 72); получив STARTTLS\r\n, зовите StreamAttach и обновляйтесь до TLS (остальные команды до обновления отрабатывают эхом в открытом режиме). Подсказка: один и тот же объект соединения до и после обновления — смотрите клиентский путь starttls в stream_tour. Критерии приёмки: на открытой стадии команды нормально отдаются эхом; после обновления открытые команды отвергаются; openssl через подсказку starttls завершает обновление и зашифрованную сессию; счётчик соединений и ресурсы обеих стадий вычищены верно.

Итоги

ТемаКратко
Комбинированный объектсервер StreamAccept (один шаг в колбэке Accept) / клиент со встроенным attach в TlsDial
Событийная модельOpen/Read/Writable/Close изоморфны TCP-потоку; исполнение на потоках Worker
Жёсткое противодавлениеостановили потребление = остановили приём; возврат Span + ожидание Writable; память отвязана от самого медленного
Прикладной интерфейсSend (частичный приём) / Available/Buffer/Front/Consume / Close / Abort / State
Транспортный мостStreamTransport одалживает TCP-поток — по надобности, без кеша; отправка через парадную дверь слоя TLS
Разделение конфигурацийxtlsserverconfig (протокольная) / xtlsstreamconfig (потоковая); разделяемый подсчёт ссылок
Сервисный жизненный циклединый выпуск разделяемой сборки после «все соединения + слушатель» в ноль (атомарная сходка)
Одиночный жизненный циклClose/Abort → ждать CLOSED/FAILED → Destroy (правило главы 82)
Автоматизация рукопожатияFeed/Drive исполняет комбинированный слой, приложение не видит шифртекста
ОбрезкаXRT_MODULE_TLS_STREAM независимо от сессионной машины; stream_tour — самопроверка всех возможностей

📖 Справочники API в этой главе:tls (201 записей)tls_stream (67 записей)net (247 записей)

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