Client Streaming 是最稳妥的选择,因 Unary RPC 会触发 OOM 或流重置;需分块传输 XmlChunk(含 chunk_id、is_first 等字段),服务端用 SAX 流式解析并校验编码与完整性。

gRPC流式传输完全适用于大XML文件,但不能直接把整个XML字符串塞进 bytes 字段——必须分块、带结构、可恢复,否则会触发内存溢出或HTTP/2流重置。
为什么不能用 Unary 传大XML?
Unary RPC 要求一次请求/响应全部加载进内存。一个 200MB 的 XML 文件在 Go 中反序列化为 struct 或甚至仅作为 []byte 持有,极易触发 OOM;gRPC 默认消息大小限制(4MB)也会直接报错 grpc: received message larger than max (xxxx vs. 4194304)。
- Unary 不支持中断恢复,网络抖动即失败重传整份
- 无法边接收边解析(如 SAX 式流式解析),丧失低延迟优势
- 服务端无法对未完成的 XML 做校验或预处理(如命名空间检查、schema 验证)
Client Streaming 是最稳妥的选择
客户端流(rpc UploadXml(stream XmlChunk) returns (UploadResult))让客户端控制节奏:按标签边界或固定字节切分、逐块发送,服务端可实时 SAX 解析或暂存为临时文件。
-
XmlChunk必须包含明确上下文字段,例如:chunk_id、is_first、is_last、offset(用于断点续传) - 块大小建议设为
512KB~1MB:太小增加 gRPC 头部开销;太大仍可能触发流控或超时 - 避免在每块开头重复写
或根标签 —— 只在is_first=true块中发一次
message XmlChunk {
uint32 chunk_id = 1;
bool is_first = 2;
bool is_last = 3;
uint64 offset = 4; // 当前块起始字节偏移
bytes data = 5; // 纯 XML 片段(不补全标签)
}
服务端解析时务必用 SAX / streaming parser
收到流后,不要拼接所有 data 再用 DOM 解析。Go 推荐 encoding/xml.Decoder,Python 用 xml.sax 或 defusedxml.sax,Java 用 StAX —— 它们能边读边处理,内存占用恒定。
- Decoder 必须复用同一个实例,跨块调用
DecodeToken(),否则会丢失命名空间/前缀状态 - 若某块末尾截断了开始标签(如
),需缓存不完整片段,合并到下一块再解析 - 服务端应记录每个
chunk_id的接收状态到 Redis 或本地 LSM(如 Badger),供GetUploadProgress查询
断点续传与完整性校验的关键细节
XML 文件一旦传输中断,从哪续、续多少、续完是否合法,全靠三件事对齐:客户端块序号、服务端已收偏移、最终 XML 校验和。
- 客户端每次发送前,先调用
GetUploadProgress(filename),服务端返回last_chunk_id和expected_offset - 服务端写入磁盘时,使用
os.O_APPEND | os.O_CREATE打开文件,确保多并发上传不覆盖 - 全部接收完成后,服务端启动一次完整校验:
sha256sum对比客户端上传前计算的哈希,同时用xmllint --noout --schema schema.xsd temp.xml验证结构
最容易被忽略的是 XML 编码一致性:客户端必须声明 ,且所有分块都按 UTF-8 编码发送;服务端 decoder 初始化时要显式指定 xml.NewDecoder(reader).CharsetReader = charset.NewReaderLabel,否则中文或特殊字符会乱码。










