Ориентация главы
xreader/xwriter — потоковая абстракция XRT: один и тот же интерфейс ReadFull/WriteFull/Seek работает поверх буферов памяти, файлов и сетевых потоков — бизнес-код пишется один раз, пункт назначения решает адаптер. Семантика Full — её забота о вас: прочитать или записать ровно указанное число байтов, частичное выполнение — отказ; заготовка цикла повторов на случай короткого чтения/записи исчезает. Глава разбирает ключевые контракты Reader/Writer, встроенные адаптеры (буфер памяти, файл, собственные обратные вызовы) и построчный читатель — высокочастотный сборный узел.
41.1引入
Коду сериализации нужно выдать продукт: в тестах — в буфер памяти (утверждение по содержимому), на диск — в файл, в сеть — в поток сокета. Прямой вызов fwrite — три пункта назначения, три экземпляра кода; собственный «интерфейс записи» — и вы заново изобретаете модуль io. xwriter стандартизует это изобретение: бизнес-код программствует против интерфейса (xrtWriterWriteFull(W, 数据, 长度) (W, данные, длина)), а пункт назначения — выбор адаптера в одну строку: xrtWriterFromBuffer (память), файловый адаптер (глава 45) или ваш адаптер на обратных вызовах (демонстрация в образце stream_tour).
Семантика Full решает другую заготовку, которую пишут каждый день: fwrite может записать мало (написано 3 байта из 10 запрошенных) — учебник учит циклу while; WriteFull забирает цикл в библиотеку: либо все 10 байтов записаны, либо отказ, и возвращаемое значение — окончательный вердикт. Сторона чтения симметрична, семантика EOF у ReadFull явная (прочитано меньше запрошенного — отказ: EOF не ошибка, но «не дочитано» — отказ).
41.2概念
Два интерфейса и семантика Full
Разделение Read и ReadFull: первый «читает сколько может» (потоковое потребление — взял кусок, обработал кусок); второй «читает ровно запрошенное, иначе отказ» (структурное чтение — заголовок сообщения фиксированно 16 байтов, значит читаем ровно 16). Сторона записи симметрична. Мнемоника выбора: потоковая обработка — обычная версия (сколько взял, столько обработал); фиксированные структуры — версия Full (недобор — отказ) — обе семантики ясны; ловушка — смешение.
Адаптер памяти: буфер как пункт назначения
xrtWriterFromBuffer(&tBuffer) адаптирует xbuffer главы 15 в Writer — заимствование, а не владение: после уничтожения адаптера буфер жив, ритм Take для забора содержимого не меняется. Это позволяет «сперва накопить в памяти, потом решить куда» и «писать сразу в файл» делить один и тот же код сериализации — тесты утверждают по буферу, продакшен меняет файловый адаптер, бизнес-код без правок.
Построчный читатель: стандартная деталь потокового текста
xlinereader (образец line) оборачивает произвольный Reader в «построчную итерацию» — разрезание по строкам, строки-представления заимствованы, конец по EOF. Общее основание сканирования журналов, построчной конфигурации и протоколов с кадрированием по строкам (прелюдия главы 72). Разделение труда со строчным разрезанием строк главы 25: полный текст уже в памяти → Split строк; строки приходят из потока → построчный читатель (целиком грузить не нужно).
Собственные адаптеры: обратный вызов как интерфейс
Когда у вашего пункта назначения нет встроенного адаптера (шифровальный конвейер, слой сжатия, сетевой протокол), выход — адаптер на обратных вызовах: предоставьте колбэки read/write и пользовательские данные, контракт интерфейса прежний. Собственный reader/writer образца stream_tour демонстрирует полную форму (включая момент вызова колбэка Close — утверждение custom close=1).
41.3示例
Полная программа: Writer в памяти и точечная перезапись
Из образца репозитория examples/io/buffer/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xbuffer Buffer; /* 底层存储:分段字节缓冲 */ xwriter* pWriter; /* 适配器:借用 Buffer,Destroy 不释放它 */ int iResult = 1; if ( !xrtBufferInit(&Buffer) ) { return 1; } */ pWriter = xrtWriterFromBuffer(&Buffer); if ( (pWriter != NULL) && xrtWriterWriteFull(pWriter, "hello world", 11u, NULL) && xrtWriterSeek(pWriter, 6, XSEEK_START, NULL) && xrtWriterWriteFull(pWriter, "xrt", 3u, NULL) ) { printf("%.*s\n", (int)Buffer.Size, (const char*)Buffer.Data); iResult = 0; } /* 两级清理:适配器归适配器,缓冲归缓冲。 */ xrtWriterDestroy(pWriter); xrtBufferUnit(&Buffer); return iResult; }
Что только что произошло. (1) Адаптер xrtWriterFromBuffer заимствует буфер — каждая запись WriteFull ложится прямо в него (дополнением), ноль промежуточных копий. (2) Seek(XSEEK_START, 偏移) (смещение) перезаписывает в точке: вернуться и поправить уже записанные байты — поза протокольного кодирования «записали заголовок, потом вернулись заполнить поле длины» (та же семья, что BufferWrite главы 15, но через единый интерфейс). (3) WriterDestroy возвращает только адаптер — сам буфер жив, содержимое читается и печатается напрямую. Во всём коде ни одного fwrite/memcpy: решение о пункте назначения сжато в строку адаптера.
Полная программа: построчный читатель
Из examples/io/line/main.c — построчное потребление произвольного Reader-а:
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 混合行尾 + 末行无换行的日志样本。 */ static const char sLog[] = "INFO server started\r\n" "WARN queue is busy\n" "ERROR request failed"; xreader* pReader; xlinereader* pLines; xlineview Line; /* 借用:到下一次 Next 前有效 */ xlinenext Next; size_t iCount = 0u; int iResult = 1; */ pReader = xrtReaderFromMemory(XRT_BYTES_LITERAL(sLog)); pLines = xrtLineReaderTake(&pReader, 1024u); if ( pLines == NULL ) { xrtReaderDestroy(pReader); return 1; } /* 逐行消费:每行是借用视图,处理完再取下一行。 */ while ( (Next = xrtLineReaderNext(pLines, &Line)) == XLINE_NEXT_LINE ) { printf("%zu: %.*s\n", ++iCount, (int)Line.Text.Size, Line.Text.Data); } if ( Next == XLINE_NEXT_END ) { iResult = 0; } /* 接管模式:一个 Destroy 连底层 Reader 一起关闭。 */ if ( !xrtLineReaderDestroy(pLines) ) { iResult = 1; } return iResult; }
Что только что произошло. (1) Память сперва адаптируется в Reader (xrtReaderFromMemory), затем принимается построчным читателем (xrtLineReaderTake передаёт владение — после этого исходный указатель Reader-а трогать нельзя); 1024 — начальная ёмкость строки, сверхдлинные строки растут автоматически, буфер расширяется только по фактической длине строки. (2) xrtLineReaderNext возвращает три состояния: XLINE_NEXT_LINE (строка взята) / XLINE_NEXT_END (EOF) / состояние отказа — смешанные концы строк (CRLF вместе с LF) и последняя строка без перевода обработаны корректно. (3) Каждая строка — заимствованное представление (поле Text у xlineview) — срок действия до следующего Next (дисциплина главы 18 прежняя). (4) В режиме приёма xrtLineReaderDestroy закрывает и нижележащий Reader — один Destroy собирает всё. Образец stream_tour оставлен для полного чтения: он проходит копирование чтения-записи, Seek/Tell, EOF, собственные адаптеры и момент Close — девять утверждений на девять групп интерфейсов.
41.4契约
- Семантика Full: прочитать/записать полностью или отказ; короткое чтение/запись — отказ, а не частичный успех; при EOF ReadFull отказывает («не дочитано» — факт, а не ошибка; семантика слота ошибки — глава 4).
- Единый интерфейс: один и тот же бизнес-код над памятью/файлами/сетью — пункт назначения решает только строка адаптера.
- Заимствованная адаптация: адаптер Buffer заимствует буфер; Destroy возвращает адаптер, не трогая нижележащий ресурс.
- Заимствованные строки: построчный читатель выдаёт представления строк, срок действия — до следующей строки.
- Собственные адаптеры: обратные вызовы + пользовательские данные реализуют любое назначение; колбэк Close срабатывает ровно один раз, при Destroy.
От примеров к инженерии: три хозяина потоковой абстракции
Сериализация в трёх средах (полная форма образца один): код структурного вывода смотрит на Writer — тесты подключают адаптер памяти для утверждений, диск подключает файловый адаптер (глава 45), сеть — адаптер протокольного потока (том 7); пункт назначения — выбор адаптера одной строкой, логика сериализации написана один раз. Конвейер разбора протоколов: сетевые байты → Reader → заголовок фиксированной длины ReadFull → построчный читатель или дочитка тела до полноты — HTTP-парсер тома 7 и есть промышленная версия этого конвейера. Обрабатывающий конвейер: Reader → слой декодирования (потоковая форма перекодирования главы 27) → бизнес-потребление — собственные адаптеры оборачивают «промежуточную обработку» в поток, слои конвейера компонуются единым интерфейсом. Три хозяина сообща подтверждают одну фразу: ценность абстракции интерфейса не в «подключается много назначений», а в «бизнес-код не движется при смене назначения».
Отношение к буферам главы 15 и файлам главы 45
Разделение трёх слоёв легко перепутать; одно сравнение всё расставляет. xbuffer (глава 15): накопитель байтов — накопил, потом забрал; у него своя семантика памяти. Адаптеры io (эта глава): приспосабливают буфер/файл/что угодно к единому интерфейсу — они не владеют данными, они конвертируют интерфейсы. Файловый модуль (глава 45): управление жизненным циклом файла (открыть/закрыть/атрибуты/асинхронность), чьи чтение и запись могут войти в единый интерфейс через адаптеры io. Отношение композиции: файловый адаптер внутри использует файловый модуль, адаптер буфера — xbuffer; слой io — интерфейсный слой над ними обоими. Выбор в коде: важно «как накапливается содержимое» → буфер напрямую; важно «единообразный интерфейс чтения-записи» → эта глава; важен «файл как ресурс» → глава 45.
Взгляд на дизайн: минимальная полнота интерфейса
Интерфейсная поверхность слоя io крошечная — чтение, запись, позиционирование, сброс, закрытие: пять действий. Но они ровно покрывают все потребности потоковых данных: это минимальная полнота — каждый интерфейс высокочастотен, удаляемой избыточности нет. На фоне «больших и полных» потоковых библиотек (десятки удобных функций) минимальная полнота окупается дважды: адаптеры пишутся легко (пяти колбэков достаточно — собственный адаптер стоит копейки), поведение выводится легко (семантических комбинаций мало, ловушек мало). Проектируя собственные слои абстракции, сверяйтесь по этой планке: точка равновесия между числом интерфейсов и выразительностью лежит там, где «ни один интерфейс нельзя удалить» — лишние удобные функции принадлежат композиции на бизнес-слое, а не членству в слое интерфейсов.
Типичные ошибки
Симптом: изредка разъезжается разбор — заголовок говорит 512 байтов, а парсер стартовал после 300 прочитанных; на низкой нагрузке всё чисто, на высокой (или медленной сети) — эпизодически.
Причина: обычный Read семантики «сколько смог»; возвращаемое значение — фактически прочитанный объём — использовано как «запрошенное пришло», и при коротком чтении структура разъезжается.
xrtReaderRead(R, Header, 16); /* сколько смог: может прочитать только 9 байтов */ Parse(Header); /* разбор 9 байтов как 16-байтовой структуры - смещение */
if ( !xrtReaderReadFull(R, Header, 16) ) { return false; /* меньше 16 байтов: EOF или прерывание - обрабатываем как отказ */ } Parse(Header);
Симптом: та же семья, что ловушки заимствования глав 18/25 — сохранённая «строка» дрейфует после следующей итерации.
Причина: построчный читатель переиспользует внутренний буфер — память, на которую указывает представление, перезаписывается следующей строкой.
const char* gFirstLine; while ( xrtLineReaderNext(pLines, &Line) == XLINE_NEXT_LINE ) { if ( gFirstLine == NULL ) { gFirstLine = Line.Data; /* сохранено представление - перезапишется следующим Next */ } }
str sFirst = NULL; while ( xrtLineReaderNext(pLines, &Line) == XLINE_NEXT_LINE ) { if ( sFirst == NULL ) { sFirst = xrtStrDupView(Line); /* чтобы жила долго - копируем */ } }
Практические задания
Напишите код сериализации (структура из двух-трёх полей); прогоните его с Writer-ом в памяти и Writer-ом в файл — продукты совпадают байт в байт; утверждение по буферной версии, фиксация на диск файловой, бизнес-код без правок.
Реализуйте чтение протокола через ReadFull: цикл «заголовок длины 4 байта + тело N байтов» — чистый выход на EOF, пропуск пустых heartbeat-сообщений нулевой длины, отказ на сверхдлинные (>1 МБ) как на злонамеренный вход.
Реализуйте собственный Writer: оборачивает другой Writer, считает записанные байты и число вызовов, тревожит по порогу (подача через журнал главы 38); Close ровно один раз (срабатывает в Destroy). Критерии приёмки: обёрнутый файловый Writer пишет 1000 записей, счётчик совпадает с фактическим размером файла; при вложении двух считающих адаптеров внутренний и внешний счётчики согласованы.
Итоги
| Тема | Кратко |
|---|---|
| Два интерфейса | Reader/Writer; Seek/Tell/Flush в комплекте |
| Семантика Full | прочитать/записать полностью или отказ; фиксированные структуры — всегда Full, потоковое потребление — обычная версия |
| Адаптация памяти | WriterFromBuffer заимствует буфер; Destroy не трогает сам буфер |
| Построчное чтение | xlinereader превращает любой Reader в построчные представления; строки заимствованы не дольше итерации; Take принимает, Destroy закрывает и нижележащий поток |
| Собственные | обратные вызовы + пользовательские данные реализуют любое назначение; Close ровно один раз при Destroy |
| Смысл | один код сериализации в трёх средах — память/файл/сеть - бизнес-код не движется при смене назначения |
| Трёхслойное разделение | xbuffer копит содержимое / слой io конвертирует интерфейсы / файловый модуль ведает ресурсом |
| Три состояния строки | NEXT_LINE строка взята / NEXT_END конец потока / состояние отказа - смешанные концы и отсутствие хвостового перевода корректны |
📖 Справочники API в этой главе:io (62 записей)buffer (24 записей)file (132 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки