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

xnetbuf — ссылочно-цепочечный буфер сетевого слоя: данные организованы цепочкой span (сегментов), а не сплошным блоком — естественная форма сетевых данных (прибытие по сегментам, частичное потребление); четыре типа добавления делятся по владению (копия/заимствование/захват/колбэк освобождения) — язык владения главы 5 полностью высажен на IO-буферы; сшивка и потребление (Pullup непрерывизирует префикс, Consume потребляет префикс — продвижение без копий); пул буферов (xnetbufpool — переиспользование сегментных блоков, сток распределений). Это прямой носитель приёма/передачи движка — SendBuffer-эхо без копий золотого стандарта главы 67 — его сцена.

65.1引入

Жизнь одного отрезка сетевых данных: прибытие сегментами (сегменты TCP, датаграммы UDP) → сборка в сообщение → частичное потребление (заголовок прочитан, тело осталось) → отправка дальше. Сплошной буфер (xbuffer главы 15) подходит под «накопить и взять», но сетевая форма «сегментное прибытие + частичное потребление» заставляет сплошность копировать на каждом шаге — заголовок пришёл, копия; тело добавилось, ещё копия; заголовок потреблён, снова перенос.

Ссылочно-цепочечный буфер меняет модель: данные живут цепочкой сегментов — прибытие становится сегментом (ноль копий), потребление продвигает указатель (ноль копий), и только когда нужен сплошной вид — Pullup (локальная копия). Четыре типа добавления сводят все источники (принятый отрезок, заголовок на стеке, блок malloc, внешняя память с колбэком) в одну цепочку — различия владения объявляются на входе и освобождаются каждое по-своему на выходе (Clear). Пул позволяет переиспользовать сегментные блоки (узлы цепочки) — счёт распределений на высокочастотных путях приёма/передачи падает почти до нуля.

65.2概念

Четыре типа добавления: полный набор владения

ВходВладениеТипичный источник
xrtNetBufAppendКопияБезопасное включение произвольной памяти
xrtNetBufAppendBorrowЗаимствованиеСтек/статические данные вызывающего (на выходе не освобождаются)
xrtNetBufAppendTakeЗахватБлок xrtMalloc (на выходе xrtFree)
xrtNetBufAppendRefКолбэкВнешняя память + функция освобождения (на выходе колбэк)

Четыре типа — буферная версия полного набора владение/заимствование/управление главы 5 (копия — вид владения, Take — передача владения, Borrow — заимствование, Ref — настраиваемое управление). Пример buf_tour собирает "hello world" из четырёх сегментов по одному на тип — с утверждением корректности освобождения: колбэк AppendRef обязан сработать ровно один раз после Clear. Prepend (вставка в голову цепочки) и Move (перенос целиком) дополняют операции формы.

Форма и потребление

1Запросы формыEmpty / Size / SpanCount / Spans — обзор цепочки
2Pullup(префикс N)непрерывизировать первые N байт (локальная кросс-сегментная копия — предпосылка разбора заголовков)
3Peek(смещение)просмотр копированием со смещением (цепочку не трогает)
4Find(байт)кросс-сегментный поиск (локация разделителя, XRT_NPOS при промахе)
5Consume(N)потребить префикс N байт (продвижение головы цепочки — продвижение без копий)

Pullup — мост между цепочкой и сплошным видом: разбору протокола нужна сплошная память для заголовков (чтение memcmp/struct) — Pullup стягивает первые N байт в первый сегмент (при нехватке копирует и сливает), остальную цепочку не трогая; Consume — продвижение без копий: потребление N байт лишь освобождает пройденные сегменты — цикл «прочитал заголовок → потребил» машины состояний протокола идёт без лишних копий. Вместе эти две операции — стандартный движок разбора протоколов (разбор заголовков HTTP главы 86 собирается из них).

Резерв и фиксация

Reserve + Commit/Cancel: резервирование N байт записываемого места в хвосте цепочки (прямой выходной путь в сеть — движок резервирует, заполняет и фиксирует в сегмент, когда надо отправить); Cancel отбрасывает незафиксированный резерв. Это канал записи без копий на стороне отправки — поза заполнения перед Send движка.

Пул буферов

xrtNetBufPoolCreate (ConfigInit настраивает размер сегмента/уровни) — сегментные блоки берутся из пула, Clear возвращает их — счёт распределений высокочастотных путей стремится к нулю (идея пулинга главы 22, высаженная на IO-сегменты). PoolTrim поджимает пустые сегменты; запросы информации пула несут счётчики занятых/свободных (поля наблюдения). Отношение пула и буфера: буфер (логика цепочки) не требует пул — голая цепочка тоже работает (malloc на сегмент); движок и высокочастотные сценарии сочетаются с пулом — PoolGet берёт буфер с сегментами, Clear автоматически возвращает в пул.

65.3示例

Полная программа: четыре типа добавления и операции цепочки

Из примера репозитория examples/network/buf_tour/main.c — шесть групп утверждений по всем интерфейсам:

examples/network/buf_tour/main.c源码 ↗































 */

#include <stdio.h>
#include <string.h>
#include <xrt.h>

/* AppendRef 的释放回调:只记录次数,不做真实释放(栈上数据)。 */
static void exampleRelease(ptr pContext, cbytes pData, size_t iSize)
{
	int* pCount = (int*)pContext;

	(void)pData;
	(void)iSize;
	++(*pCount);
}

int main(void)
{
	xnetbufpoolconfig Config;
	xnetbufpoolinfo Info;
	xnetbufpool* pPool = NULL;
	xnetbuf BufA;
	xnetbuf BufB;
	xnetspan Spans[8];
	xnetspan Span;
	xnetwspan Reserve;
	char arrText[32];
	size_t iSpans;
	size_t iGot;
	size_t iSize;
	int iReleased = 0;
	bool bInited = false;
	int iResult = 1;
	static char arrBorrow[2] = { ' ', 'w' };
	static char arrRef[2] = { 'l', 'd' };
	ptr pTaken;

	/* ---- 池生命周期:默认配置 + 统计快照。 ---- */
	xrtNetBufPoolConfigInit(&Config);
	pPool = xrtNetBufPoolCreate(&Config);
	if ( (pPool == NULL) ||
		!xrtNetBufInit(&BufA, pPool) ||
		!xrtNetBufInit(&BufB, pPool) ) {
		goto Cleanup;
	}
	bInited = true;
	if ( !xrtNetBufEmpty(&BufA) ) {
		goto Cleanup;
	}
	xrtNetBufPoolGet(pPool, &Info);
	if ( (Info.LiveBlocks != 0u) ||
		(Info.AllocCount != 0u) ) {
		goto Cleanup;
	}
	printf("buf: pool create + info ok\n");

	/* ---- 四类追加:拼出 11 字节 "hello world"。 ---- */
	if ( !xrtNetBufAppend(&BufA, "hello", 5u) ||
		!xrtNetBufAppendBorrow(&BufA, arrBorrow, 2u) ) {
		goto Cleanup;
	}
	pTaken = xrtMalloc(2u);
	if ( (pTaken == NULL) ) {
		goto Cleanup;
	}
	memcpy(pTaken, "or", 2u);
	if ( !xrtNetBufAppendTake(&BufA, pTaken, 2u) ) {
		xrtFree(pTaken);
		goto Cleanup;
	}
	if ( !xrtNetBufAppendRef(&BufA, arrRef, 2u,
			exampleRelease, (ptr)&iReleased) ) {
		goto Cleanup;
	}
	iSize = xrtNetBufSize(&BufA);
	iSpans = xrtNetBufSpanCount(&BufA);
	if ( (iSize != 11u) ||
		(iSpans < 4u) ||
		(xrtNetBufEmpty(&BufA)) ||
		(xrtNetBufSpans(&BufA, Spans, 8u) != iSpans) ||
		(Spans[0].Size != 5u) ||
		(memcmp(Spans[0].Data, "hello", 5u) != 0) ) {
		goto Cleanup;
	}
	printf("buf: append x4 borrow/take/ref -> 11 bytes in >=4 spans\n");

	/* ---- 编辑族:前缀插入 → 拼合 → 偏移读 → 查找 → 消费。 ---- */
	if ( !xrtNetBufPrepend(&BufA, ">> ", 3u) ||
		(xrtNetBufSize(&BufA) != 14u) ||
		!xrtNetBufPullup(&BufA, 5u, &Span) ||
		(Span.Size < 5u) ||
		(memcmp(Span.Data, ">> he", 5u) != 0) ||
		(xrtNetBufPeek(&BufA, 8u, arrText, 3u) != 3u) ||
		(memcmp(arrText, " wo", 3u) != 0) ||
		(xrtNetBufFind(&BufA, 'w', 0u) != 9u) ||
		(xrtNetBufFind(&BufA, 'z', 0u) != XRT_NPOS) ||
		(xrtNetBufConsume(&BufA, 3u) != 3u) ||
		(xrtNetBufSize(&BufA) != 11u) ||
		(xrtNetBufPeek(&BufA, 0u, arrText, 11u) != 11u) ||
		(memcmp(arrText, "hello world", 11u) != 0) ) {
		goto Cleanup;
	}
	printf("buf: prepend+pullup+peek+find+consume ok\n");

	/* ---- 预留与放弃:Cancel 后总长不变。 ---- */
	if ( !xrtNetBufReserve(&BufA, 8u, &Reserve) ||
		!xrtNetBufCancel(&BufA) ||
		(xrtNetBufSize(&BufA) != 11u) ) {
		goto Cleanup;
	}
	printf("buf: reserve-cancel keeps 11 bytes ok\n");

	/* ---- Move:源的块全部转移到目标尾部。 ---- */
	if ( !xrtNetBufAppend(&BufB, "!", 1u) ||
		!xrtNetBufMove(&BufA, &BufB) ||
		!xrtNetBufEmpty(&BufB) ||
		(xrtNetBufSize(&BufA) != 12u) ||
		(xrtNetBufPeek(&BufA, 11u, arrText, 1u) != 1u) ||
		(arrText[0] != '!') ) {
		goto Cleanup;
	}
	printf("buf: move source->target ok\n");

	/* ---- 收尾:Trim 释放缓存块;Clear 触发 AppendRef 回调。 ---- */
	xrtNetBufClear(&BufA);
	xrtNetBufClear(&BufB);
	if ( iReleased != 1 ) {
		goto Cleanup;
	}
	iGot = xrtNetBufPoolTrim(pPool, 0u);
	if ( iGot < 1u ) {
		goto Cleanup;
	}
	printf("buf: trim released >=1 block, release-cb fired once\n");
	iResult = 0;

Cleanup:
	/* 失败路径同样收尾:Clear 幂等,池在块归还后再销毁。 */
	if ( bInited ) {
		xrtNetBufClear(&BufA);
		xrtNetBufClear(&BufB);
	}
	if ( pPool != NULL ) {
		xrtNetBufPoolDestroy(pPool);
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/buf_tour/main.c -lws2_32 -liphlpapibuf: pool create + info okbuf: append x4 borrow/take/ref -> 11 bytes in >=4 spansbuf: prepend+pullup+peek+find+consume okbuf: reserve-cancel keeps 11 bytes okbuf: move source->target okbuf: trim released >=1 block, release-cb fired once

Что только что произошло. (1) Создание пула и запрос информации — pool create + info. (2) Совместное выступление четырёх типов: копия 5 + заимствование 2 + захват 2 + колбэк 2 = 11 байт минимум в 4 сегментах — владение объявляется на входе, двойное утверждение числа сегментов и байт. (3) Тройка потребления: Prepend (вставка в голову), Pullup (непрерывизация префикса — предпосылка кросс-сегментного разбора заголовков), Peek/Find/Consume (просмотр/поиск/продвижение) — полная операционная панель движка разбора протоколов. (4) Reserve+Cancel: резерв и отказ — цепочка сохраняет 11 байт (резерв не загрязняет зафиксированные данные). (5) Move: цепочка-источник переносится целиком в цель, источник пустеет — передача буфера без копий (фундамент передачи ссылки главы 67). (6) Trim возвращает пустые сегменты + release-cb fired once — колбэк AppendRef сработал ровно один раз, свидетельство корректности освобождения (приёмка принуждения владения).

Полная программа: базовый приём/передача

Из examples/network/buffer/main.c — простая проверка с печатью записанного:

examples/network/buffer/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xrt.h>




















 */


/* 展示后端或协议解析器直接写入可变尺寸网络缓冲。 */
int main(void)
{
	xnetbufpool* pPool = xrtNetBufPoolCreate(NULL);
	xnetbuf Buffer;
	xnetwspan Write;
	xnetspan Read;

	if ( (pPool == NULL) || !xrtNetBufInit(&Buffer, pPool) ||
		!xrtNetBufReserve(&Buffer, 6, &Write) ) {
		return 1;
	}
	memcpy(Write.Data, "packet", 6);
	if ( !xrtNetBufCommit(&Buffer, 6) ||
		!xrtNetBufFront(&Buffer, &Read) ) {
		return 1;
	}
	printf("bytes=%zu spans=%zu data=%.*s\n",
		xrtNetBufSize(&Buffer), xrtNetBufSpanCount(&Buffer),
		(int)Read.Size, (const char*)Read.Data);
	xrtNetBufClear(&Buffer);
	return xrtNetBufPoolDestroy(pPool) ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/buffer/main.c -lws2_32 -liphlpapi(写入内容逐段打印——链的内容遍历)

Что только что произошло. (1) Базовый Append с печатью — минимальный жизненный цикл цепочки (Init → Append → просмотр → Unit). (2) Это атомарная версия buf_tour — четыре типа добавления и тройка потребления растут отсюда. Разделение двух примеров: новичок сначала запускает этот (Init/Append/Unit в три шага), buf_tour — приёмка всех интерфейсов (шесть групп ok на шесть семейств операций). SendBuffer золотого стандарта главы 67 (эхо прямой передачей цепочки — ноль копий) встретит этот тип снова в практике движка — цепочка буферов есть валюта плоскости данных движка.

65.4契约

  • Четыре типа владения: копия/заимствование/захват/колбэк — объявление на входе, каждое своё освобождение на выходе (колбэк Ref ровно один раз).
  • Форма цепочки: сегментированность без копий; сплошной вид через Pullup (локально), потребление через Consume (продвижение без копий).
  • Канал резерва: Reserve → заполнить → Commit в сегмент / Cancel отбросить — запись без копий на стороне отправки.
  • Пулинг: сегментные блоки возвращаются в пул для переиспользования; Trim поджимает; информация пула несёт занятые/свободные (наблюдение).
  • Передача Move: перенос целой цепочки без копий — буферная версия передачи ссылки (фундамент SendBuffer главы 67).

От примеров к инженерии: три хозяина цепочки буферов

Приёмно-передающая грань движка (самый горячий путь): колбэк движка вручает xnetbuf (цепочка сегментов — данные уже в сегментах, ноль копий), обработчик потребляет (Consume, продвижение без копий), эхо двигает напрямую (передача ссылки SendBuffer — центральная демонстрация золотого стандарта); сторона отправки заполняет через Reserve/Commit. Ни одной копии на всём пути — ссылочная цепочка во всей красе.Парсеры протоколов: Pullup(длина заголовка) → разбор → Consume(заголовок) → цикл — фундамент разбора заголовков HTTP главы 86; Find находит разделители (протоколы с разделителями), Peek просматривает (диспетчеризация после распознавания типа). Перенос данных: файл → сеть (блок, прочитанный асинхронно в главе 47, AppendTake в цепочку — захват без копий), сеть → файл (цепочка Move в канал записи) — обмен данными между IO-доменами с цепочкой в роли контейнера. Три хозяина делят одно: цепочка — валюта плоскости данных — в противовес дереву значений как валюте плоскости управления тома 4 (управление: деревья значений, данные: цепочки — две валюты, каждая своим каналом, рукопожатие через доменное имя (разобранные адреса-результаты)).

Разделение труда с xbuffer главы 15 и асинхронностью главы 48

Три буферных инструмента легко путаются — одна таблица ставит меру. xbuffer (глава 15): сплошной накопитель байт — «накопить и взять» (чтение файлов, накопительная стадия декодирования протоколов); семантика копий, Take уносит сплошной блок. xnetbuf (эта глава): ссылочная цепочка сегментов — «сегментное прибытие, частичное потребление» (сетевой приём/передача, машины состояний протоколов); продвижение без копий, посегментное владение. Результаты асинхронных файлов (глава 48): xfiledata — один блок данных одного IO (владеющий) — его Data можно AppendTake в цепочку (файл → сеть без копий). Выбор по форме данных: сплошные потребности — xbuffer, цепочечное потребление — xnetbuf, поблочная передача — filedata → Take в цепочку. Типичный конвейер протокольного стека: порт принимает сегменты → цепочка → разбор заголовков → тело копится в xbuffer → дерево значений — каждый инструмент на своём месте.

Взгляд дизайна: границы нулевого копирования

Вся эта глава — о нулевом копировании, но знайте его границы. Правильное место нулевого копирования: перемещение данных на горячих путях (выполняется на каждое сообщение) — движок к обработчику, обработчик дальше; разумное место копий: пересечение границ (непрерывизация протокольной границы — Pullup локально), смена владения (нестабильный источник Borrow меняется на Append-копию), малые данные (копия нескольких байт заголовка дешевле управления сегментами). Критерий: большие данные с ясным жизненным циклом → нулевое копирование; малые данные или сложные циклы → копия ради простоты. 11 байт в четырёх сегментах в buf_tour — педагогическое преувеличение: реальную гранулярность сегментов решает буферная политика движка (размер сегмента в конфигурации пула); цепочка, которую видит обработчик, уже разумно сегментирована. Цена злоупотребления нулевым копированием — умственная нагрузка (окна заморозки, контракты заимствования, колбэки освобождения) — каждый урок языка владения главы 5 выплачивается здесь.

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

Ловушка 1: заимствованный сегмент переживает источник:Ловушка 1: заимствованный сегмент переживает источник

Симптомы: из цепочки читаются повисшие данные — сегмент Borrow после освобождения источника становится диким указателем; зависит от таймингов, трудно воспроизвести.

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

错误写法
void appendStack(xnetbuf* Buf)
{
	char Header[4] = { 0, 1, 2, 3 };
	xrtNetBufAppendBorrow(Buf, Header, 4);   /* заимствуем стековый массив */
}   /* функция вернулась — кадр стека реколлектится, сегмент в цепочке повисает */
正确写法
void appendCopy(xnetbuf* Buf)
{
	char Header[4] = { 0, 1, 2, 3 };
	xrtNetBufAppend(Buf, Header, 4);   /* копия в цепочку — время жизни источника не важно */
	/* или Take (захват malloc-блока) / Ref (настраиваемое освобождение) по источнику */
}
Ловушка 2: цепочка как сплошная память без Pullup:Ловушка 2: цепочка как сплошная память без Pullup

Симптомы: редкая порча разбора заголовков — заголовок, пересекающий сегменты, читается склеенным неверно; с одним сегментом нормально, с несколькими — падает.

Причина: цепочка не гарантирует сплошности — прямой memcpy(buf, Spans[0].Data, HeaderSize) видит только первый сегмент (заголовок может стоять на двух).

错误写法
xnetspan Spans[8];   /* массив вызывающего (возвращает фактическое число сегментов) */
size_t nSpans = xrtNetBufSpans(Buf, Spans, 8);
memcpy(Header, Spans[0].Data, 8);   /* копируется только первый сегмент — порча при кросс-сегментном заголовке */
正确写法
if ( xrtNetBufSize(Buf) >= 8 ) {
	xrtNetBufPullup(Buf, 8);   /* непрерывизировать префикс 8 байт (локальная кросс-сегментная копия) */
	xnetspan Out[1];
xrtNetBufPullup(Buf, 8, Out);
memcpy(Header, Out[0].Data, 8);
}

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

Базовое: воспроизведение четырёх типов

Соберите "hello world" из четырёх сегментов по одному на тип добавления; напечатайте Size и SpanCount; после Clear удостоверьтесь, что колбэк Ref сработал ровно один раз.

Продвинутое: цикл потребления заголовков протокола

Смоделируйте TLV-поток: случайно сегментированный байтовый поток входит в цепочку → Pullup(4) читает длину → Find находит разделитель → Consume потребляет запись целиком → цикл, пока не остаётся меньше записи — стандартный цикл цепочечного разбора (репетиция главы 86).

Вызов: бенчмарк приёма/передачи с пулом

Одну и ту же нагрузку (миллион сегментов по 1 КБ входит/потребляется) прогоните на голой цепочке и цепочке с пулом — сравните число распределений (статистика главы 6) и время. Критерий приёмки: пуловая версия распределяет на два порядка меньше; время заметно выигрывает; после Trim свободные пула в нуле (чистое завершение); таблица сравнения в отчёте.

Итоги

ТемаКраткая справка
Четыре типа добавленияAppend копия / Borrow заимствование / Take захват / Ref колбэк — полный набор владения
ФормаSize / SpanCount / Spans (заполняет массив вызывающего) — обзор цепочки; Prepend вставляет в голову
Тройка потребленияPullup непрерывизация префикса / Peek просмотр / Find поиск (XRT_NPOS) / Consume продвижение без копий
РезервReserve → заполнить → Commit / Cancel — запись без копий на стороне отправки
ПулPoolGet берёт буфер с сегментами / Clear возвращает в пул / Trim поджимает — сток распределений
Moveперенос целой цепочки без копий — фундамент SendBuffer (глава 67)

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

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