grpc-cj源码揭秘(二):gRPC帧协议背后的GrpcMessageReader与GrpcMessageWriter解析逻辑
【免费下载链接】grpc-cj项目地址: https://gitcode.com/Cangjie-SIG/grpc-cj
grpc-cj 是为 Cangjie 语言打造的完整 gRPC 协议实现库。本文将带你深入它的消息收发核心——GrpcMessageReader与GrpcMessageWriter,用最通俗的方式拆解 gRPC 帧协议的读写解析逻辑,帮助新手理解"一条 gRPC 消息到底是如何在网络上被拆包和组装的"。
🧩 一、先搞懂:gRPC 消息的"信封"长什么样
gRPC 运行在 HTTP/2 之上,content-type必须为application/grpc。但网络传输的并不是裸消息体,每条消息都会被装进一个统一的"帧信封":
| 组成部分 | 长度 | 说明 |
|---|---|---|
| 压缩标志位 | 1 字节 | 0表示未压缩,1表示已压缩 |
| 消息体长度 | 4 字节 | 大端序(Big-Endian)无符号整数 |
| 消息体 | N 字节 | Protobuf 序列化后的数据(可能已压缩) |
这个简单的"1 + 4 + N"结构,就是GrpcMessageReader和GrpcMessageWriter全部逻辑围绕的中心。
✍️ 二、GrpcMessageWriter:一条消息是如何"装进信封"的
GrpcMessageWriter的职责只有一个:把消息按帧协议正确封装后写入输出流。
它的核心方法是writeMessage(见 grpc_message_writer.cj),整个流程分三步:
- 序列化:通过
Marshaller把消息对象转成字节数组(通常是 Protobuf 编码); - 可选压缩:如果构造时注入了
Compressor(如 Gzip),先压缩消息体,标志位置1;否则标志位置0; - 按序写入:依次写出
1 字节标志位 → 4 字节大端长度 → 消息体。
writeMessage(message) ├── marshaller.bytes(message) 序列化 ├── compressor ? compress(data) 可选压缩 └── 写 flag + length + payload 封装成 gRPC 帧几个值得注意的设计细节:
- 长度字段是"消息体实际长度"(压缩后或原始长度),接收方靠它精确切分数据边界;
- 标志位是单字节布尔,而不是字符串,省流量且解析零歧义;
- Writer 本身无状态,同一个实例可以在流式调用中反复调用
writeMessage发送多帧。
🔍 三、GrpcMessageReader:一条消息是如何"拆出信封"的
发送是"装信封",接收就是"拆信封"。GrpcMessageReader的核心方法readMessage(见 grpc_message_reader.cj)是写入侧的完整镜像:
- 读 1 字节标志位:若流已结束(读不到数据),返回
None,调用方以此判断消息流终止——这也是流式 RPC 优雅收尾的关键; - 读 4 字节长度:用
UInt32.readBigEndian解析出消息体长度; - 读 N 字节消息体:通过内部的
fillIn方法补齐数据; - 可选解压:如果注入了
Decompressor,先解压再交给Marshaller.parse还原为对象。
关键细节:为什么要有 fillIn 循环补齐?
fillIn(grpc_message_reader.cj)看似简单,却解决了一个网络编程的经典难题:流的读取不保证一次读完。
一次read可能只返回 10 个字节,而你需要 4 字节的长度头;一次read也可能只返回部分消息体。fillIn用while (readed < b.size)循环,不断从输入流拉取数据直到目标缓冲区填满,保证每一帧都能被完整、准确地切分——这正是帧协议解析不出错的根基。
🔗 四、调用链全景:客户端与服务端如何使用它们
理解了两个类本身,再看它们在全链路中的位置就一目了然了:
客户端(client_call_impl.cj)
- 发起调用时:根据
CallOptions中的压缩设置,从CompressorRegistry查找压缩器,构造GrpcMessageWriter并写入grpc-encoding请求头(client_call_impl.cj); - 接收响应时:从响应头读取
grpc-encoding,查找对应的Decompressor,构造GrpcMessageReader,然后循环调用readMessage直到返回None,每读出一条消息就回调listener.onMessage(client_call_impl.cj)。
服务端(grpc_request_handler.cj)
- 读请求:
GrpcRequestHandler校验content-type后,根据请求头构造GrpcMessageReader,循环readMessage逐条读取流式请求消息,直到流结束触发onHalfClose(grpc_request_handler.cj); - 写响应:
ServerCallImpl内部持有GrpcMessageWriter,业务代码每次调用sendMessage就写出一帧(server_call_impl.cj)。
压缩协商是怎么达成一致的?
双方通过 HTTP 头完成"握手":
| 请求头 | 含义 |
|---|---|
grpc-encoding | 本方消息实际使用的压缩编码 |
grpc-accept-encoding | 本方能接受的对端压缩编码列表 |
具体的压缩/解压实现由 grpc_codec.cj 中的GzipCodec与IdentityCodec(不压缩的直通实现)提供,并通过CompressorRegistry/DecompressorRegistry按名称注册查找——这就是策略模式的典型应用:帧协议的骨架不动,压缩算法可随意插拔。
📌 五、核心源码文件速查
| 文件 | 职责 |
|---|---|
| grpc_message_writer.cj | gRPC 帧写入:标志位 + 长度 + 消息体封装 |
| grpc_message_reader.cj | gRPC 帧读取:分帧、解压、反序列化 |
| marshaller.cj | 消息序列化/反序列化抽象接口 |
| compressor.cj / decompressor.cj | 压缩器/解压器抽象接口 |
| grpc_codec.cj | Gzip 与 Identity 编解码器实现 |
| client_call_impl.cj | 客户端调用:读写器装配点 |
| server_call_impl.cj / grpc_request_handler.cj | 服务端调用:读写器装配点 |
🎯 总结
gRPC 帧协议看似复杂,剥开来看就是"1 字节标志 + 4 字节长度 + 消息体"的极简结构。grpc-cj 用不到 200 行代码就实现了完整的读写闭环:Writer 负责装信封,Reader 负责拆信封,fillIn循环保证切分可靠,压缩器以可插拔方式叠加在信封之上。读懂这两个类,你就掌握了 gRPC 消息层的一半密码——下次再遇到"消息粘包/拆包"问题,这套思路同样适用。
【免费下载链接】grpc-cj项目地址: https://gitcode.com/Cangjie-SIG/grpc-cj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考