grpc-cj源码揭秘(二):gRPC帧协议背后的GrpcMessageReader与GrpcMessageWriter解析逻辑
2026/9/24 14:39:29 网站建设 项目流程

grpc-cj源码揭秘(二):gRPC帧协议背后的GrpcMessageReader与GrpcMessageWriter解析逻辑

【免费下载链接】grpc-cj项目地址: https://gitcode.com/Cangjie-SIG/grpc-cj

grpc-cj 是为 Cangjie 语言打造的完整 gRPC 协议实现库。本文将带你深入它的消息收发核心——GrpcMessageReaderGrpcMessageWriter,用最通俗的方式拆解 gRPC 帧协议的读写解析逻辑,帮助新手理解"一条 gRPC 消息到底是如何在网络上被拆包和组装的"。

🧩 一、先搞懂:gRPC 消息的"信封"长什么样

gRPC 运行在 HTTP/2 之上,content-type必须为application/grpc。但网络传输的并不是裸消息体,每条消息都会被装进一个统一的"帧信封":

组成部分长度说明
压缩标志位1 字节0表示未压缩,1表示已压缩
消息体长度4 字节大端序(Big-Endian)无符号整数
消息体N 字节Protobuf 序列化后的数据(可能已压缩)

这个简单的"1 + 4 + N"结构,就是GrpcMessageReaderGrpcMessageWriter全部逻辑围绕的中心。

✍️ 二、GrpcMessageWriter:一条消息是如何"装进信封"的

GrpcMessageWriter的职责只有一个:把消息按帧协议正确封装后写入输出流

它的核心方法是writeMessage(见 grpc_message_writer.cj),整个流程分三步:

  1. 序列化:通过Marshaller把消息对象转成字节数组(通常是 Protobuf 编码);
  2. 可选压缩:如果构造时注入了Compressor(如 Gzip),先压缩消息体,标志位置1;否则标志位置0
  3. 按序写入:依次写出1 字节标志位 → 4 字节大端长度 → 消息体
writeMessage(message) ├── marshaller.bytes(message) 序列化 ├── compressor ? compress(data) 可选压缩 └── 写 flag + length + payload 封装成 gRPC 帧

几个值得注意的设计细节:

  • 长度字段是"消息体实际长度"(压缩后或原始长度),接收方靠它精确切分数据边界;
  • 标志位是单字节布尔,而不是字符串,省流量且解析零歧义;
  • Writer 本身无状态,同一个实例可以在流式调用中反复调用writeMessage发送多帧。

🔍 三、GrpcMessageReader:一条消息是如何"拆出信封"的

发送是"装信封",接收就是"拆信封"。GrpcMessageReader的核心方法readMessage(见 grpc_message_reader.cj)是写入侧的完整镜像:

  1. 读 1 字节标志位:若流已结束(读不到数据),返回None,调用方以此判断消息流终止——这也是流式 RPC 优雅收尾的关键;
  2. 读 4 字节长度:用UInt32.readBigEndian解析出消息体长度;
  3. 读 N 字节消息体:通过内部的fillIn方法补齐数据;
  4. 可选解压:如果注入了Decompressor,先解压再交给Marshaller.parse还原为对象。

关键细节:为什么要有 fillIn 循环补齐?

fillIn(grpc_message_reader.cj)看似简单,却解决了一个网络编程的经典难题:流的读取不保证一次读完

一次read可能只返回 10 个字节,而你需要 4 字节的长度头;一次read也可能只返回部分消息体。fillInwhile (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 中的GzipCodecIdentityCodec(不压缩的直通实现)提供,并通过CompressorRegistry/DecompressorRegistry按名称注册查找——这就是策略模式的典型应用:帧协议的骨架不动,压缩算法可随意插拔。

📌 五、核心源码文件速查

文件职责
grpc_message_writer.cjgRPC 帧写入:标志位 + 长度 + 消息体封装
grpc_message_reader.cjgRPC 帧读取:分帧、解压、反序列化
marshaller.cj消息序列化/反序列化抽象接口
compressor.cj / decompressor.cj压缩器/解压器抽象接口
grpc_codec.cjGzip 与 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询