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

xbuffer и xarray предыдущей главы — одноклассники: xarray держит «элементы», xbuffer держит байты. Это стандартный строительный блок приёма сетевых пакетов, протокольного кодирования и файлового ввода-вывода — данные приходят отовсюду (непостоянная длина, непостоянный ритм), Append дописывает в хвост, Write пишет в точку (за концом автоматически добивает нулями), Take забирает собранное без копий. Глава досконально разбирает эти три действия и различает перепутываемую пару «логическая длина» и «физическая ёмкость».

15.1引入

Представьте декодер TLS-записей: данные приходят сегментами по TCP, одна запись может быть разорвана на три пакета; заголовок объявляет «длина тела 512 байт», и вы при приходе заголовка хотите знать «сколько не хватает», а при сборе тела забрать запись целиком. С буфером фиксированной длины нужно угадывать максимум записи; с голым malloc — каждое расширение есть распределение и копия; с xbuffer Append копит сегменты, Size сверяется с длиной заголовка, Take забирает полную запись — буфер обнуляется и копит следующую. Цикл «копить — собрать — забрать» есть всеобщая форма протокольной обработки, и xbuffer спроектирован под неё.

Ещё одна недооцениваемая прибыль: обнуление и переиспользование буфера отвязывает число распределений от «соединений × пиковых сообщений», делая его порядка «одновременных соединений» — один буфер на соединение, один Take на сообщение, ноль новых распределений за прогон. В статистике главы 6 отношение pooled к backing показывает форму этой кривой сразу на запуске.

Запись в точку Write решает другую протокольную боль: фиксированную разметку — она делает естественной двухфазное кодирование «сперва известные поля, потом поля, зависящие от предыдущих байтов». «Байт 5 — флаг, байты 10–14 — поле длины» — длина тела при приходе заголовка неизвестна; пишем флаг, догоняем длину, дыра между ними автоматически добита нулями. С непрерывным массивом вы сами считаете смещения, растите и заполняете; Write делает это одним шагом.

15.2概念

Три стержневых действия (запомнить первым делом)

ДействиеСемантикаТипичный сценарий
Appendдописать в конец, авто-ростнакопление приёма, потоковое чтение
Writeзапись в точку, можно за конец, дыры нулямификсированные протокольные заголовки, кодирование со смещениями
Takeсшивает в непрерывный блок и передаёт владение, буфер обнулён к переиспользованиюсобрать полную запись и забрать в обработку

Size и Data — публичные поля: логическая длина и указатель данных. Чтение прямо полями, редактирование функциями — по соглашениям прошлой главы. Сравните с Count у xarray: публичное поле массива — число элементов, буфера — число байтов; «сколько записей» против «сколько байтов» — разница, определяющая разных потребителей: массивы потребляют структурированные записи, буферы сырые байтовые потоки.

Логическая длина и физическая ёмкость

1Логическая длина Sizeзаписанные байты; продвигается Append/Write
2Физическая ёмкостьвнутреннее хранилище; только растёт (кроме Trim)
3Reserveпрерасширение ёмкости, спасает от повторных распределений при сегментном приходе
4Trimвозврат лишней ёмкости; поджать перед долгим проживанием

Reserve(4096) приходит одним шагом при известном порядке — в цикле приёма эта строка сбивает число распределений с «на каждый пакет» до «одно-два за прогон». Resize явно меняет логическую длину (рост добивает нулями, усечение режет). Пара понятий живёт во всех контейнерах, но на байтовом буфере виднее всего.

Сегментированное хранение и момент сшивки

Внутреннее хранение xbuffer может быть сегментированным — последовательные Append не обязательно живут одним куском памяти. Внешний контракт всегда «при взятии — непрерывный блок»: Take сшивает перед передачей. Компромисс дизайна ясен: фаза накопления гонится за эффективностью распределений (новый сегмент цепляется напрямую, старые данные не переезжают), фаза взятия одна платит за непрерывность (одна сшивка). Если каждый Append держал бы глобальную непрерывность, каждый приход сегмента переезжал бы всем — катастрофа в сценариях приёма. И наоборот: если после каждого Append вы читаете всё содержимое, сегментация теряет смысл — это значит, что вам нужно «буфер на каждое сообщение», а не «один буфер на много сообщений».

Отношение к xnetbuf главы 67 здесь уточняется: xnetbuf — приёмный буфер сетевого слоя (ссылки, возвраты, пулинг — том 7), xbuffer — общий байтовый накопитель. Типовой конвейер декодирования: xnetbuf принимает сегменты → копией или ссылкой в xbuffer на сборку → Take отдаёт непрерывный блок парсеру. Обязанности разные, работают в связке, не взаимозаменяемы.

Семантика владения у Take

Take возвращает владеющий непрерывный блок (указатель bytes + длина выходным параметром); логическое состояние буфера обнуляется, буфер переиспользуем; возвращенный блок освобождается xrtFree. Что значит «сшивает»: при сегментированном внутреннем хранении Take делает одну сшивку перед выдачей непрерывного блока; это гарантирует всегда непрерывную память — декодер не обрабатывает сегментацию. Это и «прямая передача приёмного буфера» сетевого потока главы 67 — одна идея на разных слоях.

15.3示例

Полная программа: дописывание, разреженная запись и передача

Из примера репозитория examples/containers/buffer/main.c:

examples/containers/buffer/main.c源码 ↗



















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	xbuffer tBuffer;
	bytes pResult;
	size_t iSize;

	if ( !xrtBufferInit(&tBuffer) ) {
		return 1;
	}

	




	 */
	if (
		!xrtBufferAppend(&tBuffer, XRT_BYTES_LITERAL("abc")) ||
		!xrtBufferWrite(&tBuffer, 5, XRT_BYTES_LITERAL("z"))
	) {
		xrtBufferUnit(&tBuffer);
		return 2;
	}

	/* 逐字节十六进制转储:61 62 63 00 00 7a。 */
	for ( size_t i = 0; i < tBuffer.Size; i++ ) {
		printf("%02x%s", tBuffer.Data[i],
			i + 1u == tBuffer.Size ? "\n" : " ");
	}

	


	 */
	pResult = xrtBufferTake(&tBuffer, &iSize, NULL);
	if ( (pResult == NULL) || (iSize != 6) ) {
		xrtBufferUnit(&tBuffer);
		return 3;
	}
	xrtFree(pResult);
	xrtBufferUnit(&tBuffer);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/containers/buffer/main.c -lws2_32 -liphlpapi61 62 63 00 00 7a

Что только что произошло. (1) XRT_BYTES_LITERAL("abc") строит байтовое представление (хвостовой ноль автоматически убран — двойник конвенции главы 3 в байтовом мире); после Append Size = 3. (2) Write(5, "z") пишет по смещению 5 — за тогдашним концом 3; смещения 3 и 4 автоматически добиты нулями: в дампе 61 62 63 00 00 7a два 00 — дыры, 7a — это 'z'. «Сперва флаг, потом длина» фиксированного заголовка — ровно эта поза. (3) Take забирает все 6 байт — владение непрерывным блоком, iSize выносит длину; буфер обнулён, Unit безопасен (двойного освобождения нет). (4) Взятый блок освобождается xrtFree — владеющая конвенция полностью согласована с главой 5.

Полная программа: полный обзор ёмкости и редактирования (чек-лист буфера)

Из examples/containers/buffer_tour/main.c — Reserve/Resize/Trim, семейство редактирования и три способа взятия:

examples/containers/buffer_tour/main.c源码 ↗
















 */

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

#define BV(x) ((xbytesview) { (cbytes)(x), sizeof(x) - 1u })

int main(void)
{
	xbuffer* pBuffer = NULL;
	xbuffer* pTaken = NULL;
	xbuffer* pFrom = NULL;
	bytes pData = NULL;
	bytes pSlot = NULL;
	bytes pWrite = NULL;
	int iResult = 1;

	/* ---- 容量族:Reserve / Resize / Trim ---- */
	pBuffer = xrtBufferCreate();
	if ( (pBuffer == NULL) ||
		!xrtBufferReserve(pBuffer, 16u) ||
		!xrtBufferResize(pBuffer, 4u) ||
		(xrtBufferView(pBuffer).Size != 4u) ||
		!xrtBufferTrim(pBuffer) ||
		(xrtBufferView(pBuffer).Size != 4u) ) {
		goto Cleanup;
	}
	/* Resize 扩张填零 + 收缩截断。 */
	if ( !xrtBufferResize(pBuffer, 2u) ||
		(xrtBufferView(pBuffer).Size != 2u) ) {
		goto Cleanup;
	}
	printf("buffer: reserve/resize/trim size=%zu\n",
		xrtBufferView(pBuffer).Size);

	/* ---- 编辑族 ---- */
	/* Assign:整体替换内容。 */
	if ( !xrtBufferAssign(pBuffer, BV("abcdef")) ||
		(xrtBufferView(pBuffer).Size != 6u) ||
		(memcmp(xrtBufferView(pBuffer).Data, "abcdef",
			6u) != 0) ) {
		goto Cleanup;
	}
	/* Insert:中段插入 "XY" → abXYcdef。 */
	if ( !xrtBufferInsert(pBuffer, 2u, BV("XY")) ||
		(xrtBufferView(pBuffer).Size != 8u) ||
		(memcmp(xrtBufferView(pBuffer).Data, "abXYcdef",
			8u) != 0) ) {
		goto Cleanup;
	}
	/* Remove:删 "XY" → abcdef。 */
	if ( !xrtBufferRemove(pBuffer, 2u, 2u) ||
		(xrtBufferView(pBuffer).Size != 6u) ||
		(memcmp(xrtBufferView(pBuffer).Data, "abcdef",
			6u) != 0) ) {
		goto Cleanup;
	}
	/* Add:尾部扩展 2 字节直写 "gh"。 */
	pWrite = xrtBufferAdd(pBuffer, 2u);
	if ( (pWrite == NULL) ||
		(xrtBufferView(pBuffer).Size != 8u) ) {
		goto Cleanup;
	}
	memcpy(pWrite, "gh", 2u);
	/* AppendByte:单字节追加。 */
	if ( !xrtBufferAppendByte(pBuffer, '!') ||
		(xrtBufferView(pBuffer).Size != 9u) ||
		(memcmp(xrtBufferView(pBuffer).Data, "abcdefgh!",
			9u) != 0) ) {
		goto Cleanup;
	}
	
	 * 返回首地址由调用方写入 → 写 "ZZZ" → abZZZcdefgh!。 */
	{
		bytes pSpace = xrtBufferInsertSpace(pBuffer, 2u, 3u);

		if ( (pSpace == NULL) ||
			(xrtBufferView(pBuffer).Size != 12u) ) {
			goto Cleanup;
		}
		memcpy(pSpace, "ZZZ", 3u);
		if ( memcmp(xrtBufferView(pBuffer).Data, "abZZZcdefgh!",
				12u) != 0 ) {
			goto Cleanup;
		}
	}
	printf("buffer: edit add+insert+remove+append assign=9\n");

	/* ---- Clear:清空有效内容,容量保留。 ---- */
	xrtBufferClear(pBuffer);
	if ( xrtBufferView(pBuffer).Size != 0u ) {
		goto Cleanup;
	}

	/* ---- 接管族 ---- */
	/* SetTake:接管 xrtMalloc 内存替换缓冲内容。 */
	pData = (bytes)xrtMalloc(6);
	if ( pData == NULL ) {
		goto Cleanup;
	}
	memcpy(pData, "takeit", 6u);
	pSlot = pData;
	if ( !xrtBufferSetTake(pBuffer, &pSlot, 6u, 6u) ||
		(pSlot != NULL) ||  /* 来源槽被清空 */
		(xrtBufferView(pBuffer).Size != 6u) ||
		(memcmp(xrtBufferView(pBuffer).Data, "takeit",
			6u) != 0) ) {
		goto Cleanup;
	}
	pData = NULL;
	/* CreateTake:创建新缓冲并接管。 */
	pData = (bytes)xrtMalloc(3);
	if ( pData == NULL ) {
		goto Cleanup;
	}
	memcpy(pData, "own", 3u);
	pSlot = pData;
	pTaken = xrtBufferCreateTake(&pSlot, 3u, 3u);
	if ( (pTaken == NULL) ||
		(pSlot != NULL) ||
		(xrtBufferView(pTaken).Size != 3u) ||
		(memcmp(xrtBufferView(pTaken).Data, "own",
			3u) != 0) ) {
		goto Cleanup;
	}
	pData = NULL;
	/* From:复制创建(来源不受影响)。 */
	pFrom = xrtBufferFrom(BV("cp"));
	if ( (pFrom == NULL) ||
		(xrtBufferView(pFrom).Size != 2u) ||
		(memcmp(xrtBufferView(pFrom).Data, "cp", 2u) != 0) ) {
		goto Cleanup;
	}
	printf("buffer: take set-take=6 create-take=3 from=2\n");
	iResult = 0;

Cleanup:
	xrtFree(pData);
	xrtBufferDestroy(pFrom);
	xrtBufferDestroy(pTaken);
	xrtBufferDestroy(pBuffer);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/containers/buffer_tour/main.c -lws2_32 -liphlpapibuffer: reserve/resize/trim size=2buffer: edit add+insert+remove+append assign=9buffer: take set-take=6 create-take=3 from=2

Что только что произошло. (1) Первая строка проверяет тройку ёмкости: Reserve прерасширяет, Resize фиксирует длину, Trim поджимает при неизменной семантике Size — заметьте деталь «Size после Trim не меняется»: усечение ёмкости срезает только свободное место, записанное содержимое нетронуто. (2) Вторая строка проверяет семейство редактирования: добавить, вставить, удалить, дописать и Assign (целиком заменить содержимое) выполняются по порядку с верными значениями. (3) Третья строка сводит три позы взятия: Take на существующем буфере, постройка-и-взятие на новом, преобразование из представления — три пути сходятся в непрерывный владеющий блок. Девяносто процентов сетевого кода используют первую: один переиспользуемый буфер + один Take на сообщение.

Три действия в реальном ритме

Поодиночке Append/Write/Take — три функции; в ритме протокольной обработки это три доли одного цикла. Возьмём «каждое сообщение = 4-байтовый заголовок длины + тело»: любой пришедший сегмент сперва Append в буфер; Size сверяется с «объявленной заголовком длиной», решая, собрано ли сообщение; собрано — Take парсеру, буфер обнулён под следующее. В цикле трёх долей буфер — единственный носитель состояния: без списка сообщений, без пообъектных распределений, без двойных копий. В главе 66 этот цикл увидите работающим в колбэках сетевого движка с миллионной частотой в секунду — по форме один в один с игрушечной версией здесь.

15.4契约

  • Публичные поля: строится через XRT_BYTES_LITERAL; Size (логическая длина) и Data (указатель) читаются напрямую; редактирование функциями.
  • Write добивает нулями: дыры за концом автоматически нули; логическая длина прыгает к концу записи.
  • Передача Take: возвращает владеющий непрерывный блок (xrtFree); буфер обнулён и переиспользуем; Unit не даёт двойного освобождения.
  • Отказ без побочных эффектов: неудачное редактирование не трогает буфер; очистка через Unit всегда безопасна.
  • Дисциплина ёмкости: известен порядок — сперва Reserve; перед долгим проживанием — Trim; Trim срезает только свободное место, не записанное.

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

Обе ловушки крутятся вокруг одного: Size и Data — «снимок (снапшот) момента чтения», и любое редактирование (Append/Write/Take) может сделать его просроченным. Поняв это, вы видите: обе ловушки — одна в двух видах.

Ловушка 1: чтение старого Size после Take:Ловушка 1: чтение старого Size после Take

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

Причина: Take обнуляет буфер к переиспользованию — Size до вызова устарел, после уже 0; использовать старую переменную как длину — ошибка по построению.

错误写法
size_t iWant = tBuffer.Size;          /* сохранили старую длину */
bytes pBlock = xrtBufferTake(&tBuffer, &iSize, NULL);
process(pBlock, iWant);               /* iWant — длина до Take? совпадение, не правило */
正确写法
bytes pBlock = xrtBufferTake(&tBuffer, &iSize, NULL);
if ( pBlock == NULL ) {
	return false;
}
process(pBlock, iSize);              /* длина из выходного параметра Take — всегда истинная длина этого взятия */
Ловушка 2: удержание указателя Data за ростом:Ловушка 2: удержание указателя Data за ростом

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

Причина: Data указывает во внутреннее хранилище; Append/Write/Reserve могут запустить переселение; у указателя, как и у длины, есть срок.

错误写法
bytes pView = tBuffer.Data;            /* запомнили указатель */
xrtBufferAppend(&tBuffer, pMore, 4096);/* рост переселяет */
consume(pView, tBuffer.Size);          /* pView повис */
正确写法
xrtBufferAppend(&tBuffer, pMore, 4096);
consume(tBuffer.Data, tBuffer.Size);  /* берём в момент использования; указатель и длина одного источника и времени */

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

Базовое: воспроизвести дамп

Сперва с закрытой книгой выпишите шесть ожидаемых байтов на бумаге; затем вручную построюйте 61 62 63 00 00 7a по главному примеру: допишите три байта, запишите один по смещению 5, сдампите hex по байтам, подтверждая дыры нулей.

Продвинутое: скелет TLV-декодера (первая игрушка мира протоколов)

Реализуйте минимальный TLV (тип-длина-значение) накопитель на xbuffer: feed(字节段) (подать байтовый сегмент) дописывает; next() проверяет, собрана ли полная запись (4-байтовый заголовок объявляет длину), и собранная Take забирает её, остаток буфера двигается дальше. Подсказка: Take берёт всё — когда нужно «взять первые N байтов», сперва запомните границу записи, потребляйте представлением, потом обрабатывайте целиком.

Вызов: сравнение производительности приёма (цифры говорят)

Напишите имитационный цикл приёма: случайные сегменты 64–1500 байт, всего 10 МБ, накапливая в двух позах — «без Reserve» и «Reserve(1МБ) на старте» — с Take каждые 64 КБ. Сравните системные распределения двух версий статистикой главы 6. Критерий приёмки: у версии с Reserve распределений значимо меньше; окончательные сборки обеих версий побайтово идентичны.

Итоги

ТемаКраткая справка
Три действияAppend в хвост / Write в точку с нулями / Take безкопиевая передача
Публичные поляSize логическая длина, Data указатель; просрочены любым редактированием
ЁмкостьReserve прерасширение, Resize фиксация, Trim поджатие
Семантика Takeвозвращает владеющий блок (xrtFree), буфер обнулён к переиспользованию
Построение представленийXRT_BYTES_LITERAL отдаёт литералы прямо в Append/Write
Протокольный циклAppend копит → Size сверяет → Take забирает → буфер переиспользуется
Разделение с массивоммассивы потребляют структурированные записи (число элементов), буферы сырые байты (длина)
Против xnetbufxnetbuf принимает сегменты (сетевой слой), xbuffer собирает (общий) — конвейерная пара
ЛитералыXRT_BYTES_LITERAL автоматически убирает хвостовой ноль; Append/Write едят напрямую; константные сегменты сшиваются без промежуточных распределений

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

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