Ориентация главы
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 (перенос целиком) дополняют операции формы.
Форма и потребление
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 — шесть групп утверждений по всем интерфейсам:
*/ #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; }
Что только что произошло. (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 — простая проверка с печатью записанного:
#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; }
Что только что произошло. (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 выплачивается здесь.
Типичные ошибки
Симптомы: из цепочки читаются повисшие данные — сегмент 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 (настраиваемое освобождение) по источнику */ }
Симптомы: редкая порча разбора заголовков — заголовок, пересекающий сегменты, читается склеенным неверно; с одним сегментом нормально, с несколькими — падает.
Причина: цепочка не гарантирует сплошности — прямой 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки