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

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

1ReaderRead/ReadFull/Seek/Tell - прочитать полностью или отказ
2WriterWrite/WriteFull/Seek/Flush - записать полностью или отказ
3АдаптерыBuffer (память) / File (диск) / собственный обратный вызов - одна строка меняет пункт назначения
4ОсвобождениеDestroy адаптера возвращает сам адаптер, не трогая нижележащий ресурс

Разделение 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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/io/buffer/main.c -lws2_32 -liphlpapihello xrtld

Что только что произошло. (1) Адаптер xrtWriterFromBuffer заимствует буфер — каждая запись WriteFull ложится прямо в него (дополнением), ноль промежуточных копий. (2) Seek(XSEEK_START, 偏移) (смещение) перезаписывает в точке: вернуться и поправить уже записанные байты — поза протокольного кодирования «записали заголовок, потом вернулись заполнить поле длины» (та же семья, что BufferWrite главы 15, но через единый интерфейс). (3) WriterDestroy возвращает только адаптер — сам буфер жив, содержимое читается и печатается напрямую. Во всём коде ни одного fwrite/memcpy: решение о пункте назначения сжато в строку адаптера.

Полная программа: построчный читатель

Из examples/io/line/main.c — построчное потребление произвольного Reader-а:

examples/io/line/main.c源码 ↗























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/io/line/main.c -lws2_32 -liphlpapi1: INFO server started2: WARN queue is busy3: ERROR request failed

Что только что произошло. (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 крошечная — чтение, запись, позиционирование, сброс, закрытие: пять действий. Но они ровно покрывают все потребности потоковых данных: это минимальная полнота — каждый интерфейс высокочастотен, удаляемой избыточности нет. На фоне «больших и полных» потоковых библиотек (десятки удобных функций) минимальная полнота окупается дважды: адаптеры пишутся легко (пяти колбэков достаточно — собственный адаптер стоит копейки), поведение выводится легко (семантических комбинаций мало, ловушек мало). Проектируя собственные слои абстракции, сверяйтесь по этой планке: точка равновесия между числом интерфейсов и выразительностью лежит там, где «ни один интерфейс нельзя удалить» — лишние удобные функции принадлежат композиции на бизнес-слое, а не членству в слое интерфейсов.

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

Ловушка 1: чтение структур фиксированной длины обычным Read:Ловушка 1: чтение структур фиксированной длины обычным Read

Симптом: изредка разъезжается разбор — заголовок говорит 512 байтов, а парсер стартовал после 300 прочитанных; на низкой нагрузке всё чисто, на высокой (или медленной сети) — эпизодически.

Причина: обычный Read семантики «сколько смог»; возвращаемое значение — фактически прочитанный объём — использовано как «запрошенное пришло», и при коротком чтении структура разъезжается.

错误写法
xrtReaderRead(R, Header, 16);           /* сколько смог: может прочитать только 9 байтов */
Parse(Header);                           /* разбор 9 байтов как 16-байтовой структуры - смещение */
正确写法
if ( !xrtReaderReadFull(R, Header, 16) ) {
	return false;   /* меньше 16 байтов: EOF или прерывание - обрабатываем как отказ */
}
Parse(Header);
Ловушка 2: сохранение представлений строк из построчного читателя:Ловушка 2: сохранение представлений строк из построчного читателя

Симптом: та же семья, что ловушки заимствования глав 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-ом в файл — продукты совпадают байт в байт; утверждение по буферной версии, фиксация на диск файловой, бизнес-код без правок.

Продвинутое: читатель сообщений фиксированной длины (семантика Full на практике)

Реализуйте чтение протокола через 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 записей)

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