仓颉multipart实现原理一BufferReader如何为普通InputStream变出seek能力【免费下载链接】multipartmultipart/form-data请求体解析工具项目地址: https://gitcode.com/Cangjie-SIG/multipart本文带你拆解仓颉 multipartmultipart/form-data请求体解析工具的核心实现一个小巧的BufferReader类如何为只能向前读的普通InputStream变出 seek定位回退能力让任意流都能被解析。 先看问题multipart 解析为什么会卡住浏览器上传文件时HTTP 请求体长这样用--boundary分割线把每个字段文本或文件隔开。解析器必须一边读流、一边识别分割线。但这里有个天然矛盾流的特性HTTP 请求体像一根水管数据单向流动。仓颉的InputStream也一样——read()只能往前读过的字节收不回来算法的需求解析 multipart 时经常需要把刚才读错的几个字节放回去让主循环重新处理。比如解析某个部件的头部时如果下一行意外碰到了--boundary分割线就得把这行字节归还给主状态机否则边界就丢了。早期版本的解法是干脆要求用户传入的流必须实现Seekable如File。但这意味着socket、管道等普通流根本没法用。从 0.1.2 版本开始CHANGELOG 记录了关键调整入参从需要实现Seekable调整为普通InputStream这个普通流也能解析的能力就是由BufferReader实现的。 核心思路用一块内存换回倒带能力实现全部在 src/multipart_buffer.cj 中思路其实一句话能说清把所有读过的字节都攒在内存缓冲里seek 就只是在缓冲里挪一个游标。类声明为class BufferReader : InputStream Seekable——对外同时是可读的流和可定位的流。四个关键成员src/multipart_buffer.cj#L4-L8成员角色input用户传入的原始流任意 InputStreambufferByteBuffer只追加、不清空的数据仓库rawReadLength累计已从原始流读走的字节数bufReadPos当前读游标在缓冲中的位置三个方法分工明确readRaw(expectCount)src/multipart_buffer.cj#L33-L42只要游标之后还剩的数据不够用就以 4KB 为一块见 src/mulitpart.cj#L8 的bufSize 4096从原始流批量读入buffer直到缓存充足read(buf)src/multipart_buffer.cj#L44-L49先调readRaw保证有货再从buffer取数游标前移seek(sp)src/multipart_buffer.cj#L51-L53整个方法体只有一行——直接转发给buffer.seek(sp)底层流完全不被触碰。✨ 精髓就在这里因为buffer保存了所有读过的字节回退根本不需要真的去动 socket 或磁盘只是把游标拨回缓冲区里的某个旧位置纯内存操作快且安全。 seek 在解析器里的真实用途回退能力不是摆设它在部件头部解析中被实际使用。看 src/mulitpart_part.cj#L60-L63读取 part 头部的readMIMEHeader中if (isDashLine(line, mr.dashBoundary)) { // 分割线 r.seek(SeekPosition.Current(-line.size)) return }翻译成大白话我以为是 header 的一行结果发现是下一个部件的分割线——好seek往回退这一行的长度把字节还回去让nextPart()主循环去认领它。部件正文读取路径src/mulitpart_part.cj#L86-L88也有同样的归还动作。这正是按行读取 行级回退策略读的行足够小回退的代价也足够小。 整条流水线如何协作在 src/mulitpart.cj#L52 能看到完整的组装方式用户的 InputStream → BufferReader变出 Seekable → BufferedInputStream标准库字节级读取 → MulitpartReader三层各司其职像一场接力赛原始流跑第一棒负责从 socket/磁盘/文件交付字节BufferReader跑第二棒缓存数据、提供 seek是本文的主角标准库BufferedInputStream提供readByte()等便利接口支撑 src/mulitpart.cj#L190-L199 的逐行读取MulitpartReader跑最后一棒nextPart()src/mulitpart.cj#L144-L188作为主状态机逐行识别 boundary切分出一个个MultipartPart最终汇总成MulitpartForm见 src/multipart_form.cj。⚖️ 代价与边界seek 不是免费的操作普通流BufferReader读取真实读 socket/磁盘从内存缓冲取不够才触发input.read回退seek 回❌ 不可能✅ 游标拨回缓冲旧位置可回退范围—仅限已读入缓冲的数据两个需要心里有数的点内存换能力buffer只增不减解析到结尾时整个请求体都会驻留在内存中。这是 seek 能力的账单。好在库自身还有内存预算maxMemory策略文件部分会落盘到临时目录整体开销可控回退有边界只能退回已经读过的区域不能 seek 到尚未读入的位置——不过readRaw会在需要时自动向底层流拉取更多数据所以解析流程不会因此卡壳。 动手体验克隆仓库即可按 README.md 中的示例运行它读取 testdata/formData 这个真实请求体样本遍历form.values文本字段与form.files文件字段git clone https://gitcode.com/Cangjie-SIG/multipart你会发现对外 API 完全没变MulitpartReader(input, boundary)的input从必须可定位变成了任意 InputStream——这就是BufferReader在底层默默完成的事。 小结与预告本文一句话总结BufferReader 通过只增不减的内存缓冲 缓冲内游标把普通 InputStream 包装成支持 seek 的流从而让仓颉 multipart 解析器可以在读到分割线时把字节放回去优雅复用了经典的按行解析算法。下一篇将深入解析器的内存预算机制maxMemory如何决定文本进内存、文件落临时目录以及MultipartFile与MultipartFileStream的读取设计src/multipart_file.cj敬请期待 【免费下载链接】multipartmultipart/form-data请求体解析工具项目地址: https://gitcode.com/Cangjie-SIG/multipart创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
