本章导读
xreader/xwriter 是 XRT 的流抽象:同一套 ReadFull/WriteFull/Seek 接口,接内存缓冲、文件、网络流都行——业务代码写一次,目的地由适配器决定。Full 语义是它的贴心之处:读满写满指定字节数,部分完成即失败——短读短写的重试循环样板从此消失。本章讲 Reader/Writer 的核心契约、四类内置适配器(内存缓冲、文件、自定义回调)、以及行读取器这个高频组合件。
41.1引入
一段序列化代码要把产物写出去:测试时写内存缓冲(断言内容)、落盘时写文件、网络时写 socket 流。如果直接调 fwrite,三套目的地就是三份代码;如果自己定义"写接口",你正在重新发明 io 模块。xwriter 把这个发明标准化:业务代码面向接口编程(xrtWriterWriteFull(W, 数据, 长度)),目的地是适配器的一行选择——xrtWriterFromBuffer(内存)、文件适配器(第 45 章)、或你自己的回调适配器(stream_tour 范例演示)。
Full 语义解决另一个天天写的样板:fwrite 可能短写(写了 3 字节而你要求 10)——教科书教你 while 循环重试;WriteFull 把循环收进库:要么写满 10 字节、要么失败,返回值就是最终裁决。读侧同理,ReadFull 对 EOF 的语义明确(读到不足量即失败——EOF 不是错误但"没读满"是)。
41.2概念
两个接口与 Full 语义
Read 与 ReadFull 的分工:前者"尽力读,读到多少算多少"(流式消费——每次拿一块处理);后者"读满指定量,否则失败"(结构化读取——消息头固定 16 字节就必须读满 16)。写侧对称。选择口诀:流式处理用普通版(拿到多少处理多少)、定长结构用 Full 版(不满即失败)——两版语义都明确,混用才是坑。
内存适配器:缓冲即目的地
xrtWriterFromBuffer(&tBuffer) 把第 15 章 xbuffer 适配成 Writer——借用而非拥有:适配器销毁后缓冲还在,Take 取走内容的节奏不变。这让"先攒内存再决定去哪"与"直接写文件"共享同一段序列化代码——测试用缓冲断言、生产换文件适配器,业务代码零改动。
行读取器:流式文本的标准件
xlinereader(line 范例)把任意 Reader 包成"逐行迭代"——按行分割、行视图借用、EOF 结束。日志扫描、配置逐行、协议按行帧(第 72 章前奏)的公共底座。它与第 25 章字符串行分割的分工:已有完整文本在内存 → 字符串 Split;从流里逐行来 → 行读取器(不必整载)。
自定义适配器:回调即接口
你的目的地没有内置适配器(加密管道、压缩中间层、网络协议)时,回调适配器是出口:提供 read/write 回调与用户数据,接口契约照旧。stream_tour 范例的自定义 reader/writer 演示了完整形态(包括 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; }
刚才发生了什么。 ① xrtWriterFromBuffer 适配器借用缓冲——WriteFull 的每次写入直接落进缓冲(追加),零中间拷贝。② Seek(XSEEK_START, 偏移) 定点覆盖:回头改已写的字节——"写完头部再回头填长度字段"的协议编码姿势(与第 15 章 BufferWrite 同族,但经统一接口)。③ 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; }
刚才发生了什么。 ① 内存先适配成 Reader(xrtReaderFromMemory)、再被行读取器接管(xrtLineReaderTake 移交所有权——此后不得再碰原 Reader 指针);1024 是初始行容量,超长行自动增长、缓冲只随实际行长扩展。② xrtLineReaderNext 返回三态:XLINE_NEXT_LINE(取到行)/ XLINE_NEXT_END(EOF)/ 失败态——混合行尾(CRLF 与 LF 混用)与末行无换行都被正确处理。③ 每行是借用视图(xlineview 的 Text 字段)——时效止于下一次 Next(第 18 章纪律照旧)。④ 接管模式下 xrtLineReaderDestroy 连底层 Reader 一起关闭——一个 Destroy 收全套。把 stream_tour 留作完全体阅读:它把 Reader/Writer 的读写复制、Seek/Tell、EOF、自定义适配器与 Close 时机全部走了一遍,九个断言对应九组接口。
41.4契约
- Full 语义:读满写满或失败;短读短写是失败不是部分成功;EOF 时 ReadFull 失败("没读满"是事实不是错误——错误槽语义见第 4 章)。
- 接口统一:同一段业务代码对接内存/文件/网络——目的地只由适配器行决定。
- 借用适配:Buffer 适配器借用缓冲;Destroy 归还适配器不动底层资源。
- 行借用:行读取器产出行视图,时效止于下一行。
- 自定义适配器:回调 + 用户数据实现任意目的地;Close 回调在 Destroy 时触发恰好一次。
从示例到工程:流抽象的三个宿主
序列化三栖(示例一的完全体):结构化输出代码面向 Writer——测试接内存适配器做断言、落盘接文件适配器(第 45 章)、网络接协议流适配器(卷七);目的地是一行适配器的选择,序列化逻辑一次写成。协议解析流水线:网络字节 → Reader → 定长头 ReadFull → 行读取器或 Body 读满——卷七的 HTTP 解析器就是这条流水线的工业化版本。处理管道:Reader → 解码层(第 27 章转码的流式形态)→ 业务消费——自定义适配器把"中间处理"包装成流,管道各层用统一接口组合。三宿主共同验证同一句话:接口抽象的价值不在"能接很多目的地",在于换目的地时业务代码不动。
与第 15 章缓冲、第 45 章文件的关系
三个层次的分工容易混淆,一张对照讲清。xbuffer(第 15 章):字节累积器——攒齐再取,自己有内存语义;io 适配器(本章):把缓冲/文件/任何东西适配成统一接口——它不拥有数据,只是接口转换;文件模块(第 45 章):文件的生命周期管理(打开/关闭/属性/异步),其读写可通过 io 适配器进入统一接口。组合关系:文件适配器内部用文件模块、缓冲适配器内部用 xbuffer——io 层是它们之上的接口层。写代码的选择:关心"内容怎么攒"→ 直接用缓冲;关心"统一读写接口"→ 本章;关心"文件这个资源本身"→ 第 45 章。
一个设计观:接口的最小完备
io 层的接口面很小——读、写、定位、冲刷、关闭,五个动作。但它恰好覆盖了流式数据的全部需求:这叫最小完备——每个接口都高频使用、没有可删的冗余。对比"大而全"的流库(几十个便捷函数),最小完备的收益是:适配器好写(五个回调就够——自定义适配器的成本极低)、行为好推(语义组合少、坑位少)。写自己的抽象层时值得参照这个标准:接口数量与表达力的平衡点,在"每个接口都不可删除"处——多余的便捷函数应该是业务层的组合,不是接口层的成员。
常见错误
症状:偶发解析错位——消息头说 512 字节,实际读了 300 就开始解析;低负载正常、高负载(或慢网络)偶发。
原因:普通 Read 是尽力语义,返回值是实际读取量——直接当"读到了请求量"用,短读时结构错位。
xrtReaderRead(R, Header, 16); /* 尽力读:可能只读了 9 字节 */ Parse(Header); /* 用 16 字节结构解析 9 字节内容——错位 */
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 干净退出、长度为 0 的心跳消息跳过、超长(>1MB)按恶意输入拒绝。
实现自定义 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 个条目)
点击查看完整签名、参数约束、返回值与错误说明