
grpc-go 消息压缩实战从UseCompressor到自定义 Compressor 的完整指南【免费下载链接】grpc-goThe Go language implementation of gRPC. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-go本指南以 grpc-go 仓库中的 compression 示例 及其配套的 官方压缩文档 为核心系统讲解 gRPC 消息压缩的标准用法客户端如何通过UseCompressor指定压缩算法、服务端如何注册压缩器并自动协商解压与回压以及已被标记废弃的旧式压缩 API 与新版 API 的优先级规则。读完本文你将能够为 RPC 调用启用 gzip 压缩、把压缩设置为某个连接ClientConn的默认行为、并基于encoding.Compressor接口自定义自己的压缩算法。压缩在 gRPC 中如何工作gRPC 的消息压缩发生在应用层与 HTTP/2 传输层之间。发送方将 protobuf或其他 Codec序列化后的消息字节流先经过 Compressor 压缩再写入 HTTP/2 帧接收方则根据消息头中的grpc-encoding字段选择对应的解压器还原消息。压缩算法由字符串名字标识如gzip、identity这个名字同时用于客户端发起 RPC 时在grpc-encoding头中声明请求的压缩算法服务端解压请求消息时在已注册的压缩器表中查找匹配实现服务端回复响应时选择响应压缩算法客户端解压响应消息时再次查找匹配实现。从源码结构看这一机制的核心注册表位于 encoding/encoding.go// encoding/encoding.go type Compressor interface { // Compress writes the data written to wc to w after compressing it. Compress(w io.Writer) (io.WriteCloser, error) // Decompress reads data from r, decompresses it, and provides the // uncompressed data via the returned io.Reader. Decompress(r io.Reader) (io.Reader, error) // Name is the name of the compression codec and is used to set the // content coding header. The result must be static. Name() string } func RegisterCompressor(c Compressor) { ... } func GetCompressor(name string) Compressor { ... }RegisterCompressor以Name()返回的字符串为 key 将实现存入全局注册表同名注册时后注册者生效GetCompressor则供收发两侧按名字取用。注册表注释还明确了使用约束必须在初始化阶段init()函数内调用且不是线程安全的。推荐做法encoding.RegisterCompressorUseCompressor官方文档明确指出在客户端和服务端配置消息压缩的首选方式是使用encoding.RegisterCompressor注册一个压缩算法实现然后用UseCompressor这个CallOption在发起 RPC 时选择它。gRPC 官方已在仓库中内置了 gzip 实现位于 encoding/gzip/gzip.go本文的示例即基于它展开。客户端安装压缩器并用UseCompressor发送压缩 RPC示例 examples/features/compression/client/main.go 展示了完整流程import ( google.golang.org/grpc google.golang.org/grpc/credentials/insecure google.golang.org/grpc/encoding/gzip // 安装 gzip 压缩器副作用导入触发 init pb google.golang.org/grpc/examples/features/proto/echo ) func main() { conn, err : grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) ... c : pb.NewEchoClient(conn) const msg compress ctx, cancel : context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // 关键一行通过 CallOption 指定本次 RPC 使用 gzip 压缩 res, err : c.UnaryEcho(ctx, pb.EchoRequest{Message: msg}, grpc.UseCompressor(gzip.Name)) fmt.Printf(UnaryEcho call returned %q, %v\n, res.GetMessage(), err) }注意其中两点import google.golang.org/grpc/encoding/gzip是副作用导入gzip包在init()中调用encoding.RegisterCompressor完成自我注册因此只需导入即可使用_前缀导入同样有效见下文服务端。每次调用都通过grpc.UseCompressor(gzip.Name)显式指定压缩方式。gzip.Name是包内常量值为字符串gzip见 encoding/gzip/gzip.go。UseCompressor在 rpc_util.go 中定义返回一个CompressorCallOption// rpc_util.go // UseCompressor returns a CallOption which sets the compressor used when // sending the request. If WithCompressor is also set, UseCompressor has // higher priority. func UseCompressor(name string) CallOption { return CompressorCallOption{CompressorType: name} }其注释与官方文档互相印证与旧 APIWithCompressor同时设置时UseCompressor优先级更高。让连接上所有 RPC 默认使用压缩如果希望某个ClientConn上的所有调用都默认压缩不必逐个 RPC 添加CallOption而是用WithDefaultCallOptions这个DialOption把UseCompressor固化为默认值。示例代码的注释也给出了这一写法// 若客户端上所有 RPC 都应以此方式发送使用 DialOption // grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)) grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)), )WithDefaultCallOptions定义于 dialoptions.go实现上就是把传入的CallOption追加到dialOptions.callOptions切片中在每次调用时作为默认选项生效。示例代码本身保留了“单次调用显式指定”的写法并在注释中给出“全部调用默认压缩”的替代方案两者可互为补充默认压缩保证兜底单次调用的UseCompressor则可按需覆盖默认值例如对极小的消息改用不压缩。服务端注册压缩器后自动协商服务端无需为每个 RPC 指定压缩方式。只要注册了压缩器它就会自动用于解压请求消息并在响应时使用与客户端请求相同的压缩算法回压。示例 examples/features/compression/server/main.go 中通过空导入完成注册import ( google.golang.org/grpc // 安装 gzip 编码会将它注册为一个可用压缩器。 // 若客户端支持gRPC 会自动协商并使用 gzip。 _ google.golang.org/grpc/encoding/gzip pb google.golang.org/grpc/examples/features/proto/echo ) func main() { lis, err : net.Listen(tcp, fmt.Sprintf(:%d, *port)) ... s : grpc.NewServer() pb.RegisterEchoServer(s, server{}) s.Serve(lis) }服务端从grpc-encoding头中读取客户端声明的压缩算法名用encoding.GetCompressor在注册表中查找对应实现找到则自动完成解压并在响应用同样的算法压缩。这一点可以在 server.go 的处理逻辑中看到印证服务端先根据请求的grpc-encoding获取解压器若获取不到则返回Unimplemented随后若没有显式设置旧式RPCCompressor则复用请求的压缩方法压缩响应。未注册压缩器时的错误语义文档明确规定了压缩器缺失时的两种错误码二者方向相反、容易混淆值得单独强调场景错误码触发时机客户端指定了未注册的压缩器codes.InternalRPC 发送前即返回给应用服务端收到使用了未注册压缩器的请求codes.Unimplemented服务端处理请求时返回给客户端客户端侧的Internal错误可以在 rpc_util.go 的解压/压缩协商逻辑中找到源码依据在客户端isServer false解压响应时若找不到对应压缩器返回Internal错误而在服务端isServer true解压请求时找不到压缩器则返回Unimplemented错误。服务端侧同样在 server.go 中构造了Unimplemented: grpc: Decompressor is not installed for grpc-encoding %q状态码并直接写回对端。提示UseCompressor(identity)是一个特例。identity表示“不压缩”见 encoding/encoding.go 中的Identity常量此时虽然不进行压缩但identity仍会被写入grpc-encoding头发送给服务端。该常量注释注明“仅供 gRPC 内部使用”普通场景不必手动设置。自定义压缩算法以 gzip 实现为模板内置的 gzip 包本身就是一个绝佳的“自定义压缩器”模板。其核心结构见 encoding/gzip/gzip.go// 包初始化时完成注册 func init() { c : compressor{} c.poolCompressor.New func() any { return writer{Writer: gzip.NewWriter(io.Discard), pool: c.poolCompressor} } encoding.RegisterCompressor(c) } func (c *compressor) Compress(w io.Writer) (io.WriteCloser, error) { z : c.poolCompressor.Get().(*writer) z.Writer.Reset(w) return z, nil } func (c *compressor) Decompress(r io.Reader) (io.Reader, error) { // 优先从对象池取出可复用的 gzip.Reader否则新建 ... } func (c *compressor) Name() string { return Name // gzip } type compressor struct { poolCompressor sync.Pool poolDecompressor sync.Pool }要编写自己的压缩算法只需实现encoding.Compressor接口的三个方法Compress、Decompress、Name并在init()中调用encoding.RegisterCompressor注册即可。值得借鉴的工程细节对象池复用sync.Pool缓存gzip.Writer/gzip.Reader避免高并发下频繁创建对象带来的 GC 压力writer.Close()在归还池子的同时关闭底层 Writer见 encoding/gzip/gzip.go。响应实现io.ReadCloserDecompress返回的io.Reader可选择性实现io.ReadCloser若实现了gRPC 会恰好调用一次Close()见 encoding/encoding.go 的接口注释。可调参数gzip 包额外提供了SetLevel(level int)函数可在初始化阶段调整压缩级别范围gzip.DefaultCompression到gzip.BestCompressionHuffmanOnly不支持返回非 nil error 表示级别非法见 encoding/gzip/gzip.go。该函数不是线程安全的必须只在init()阶段调用。导入即注册通过空导入_ your/package/compressor即可让任意一方客户端或服务端获得该压缩算法的支持。已废弃的旧式压缩 API 及其优先级规则在新版encoding.RegisterCompressor方案出现之前gRPC 提供了基于接口的旧式压缩 API。官方文档明确不推荐使用但存量代码可能仍在运行理解它在新旧 API 并存时的优先级规则有助于迁移和排查问题。客户端旧式 APIfunc WithCompressor(grpc.Compressor) DialOption // 旧设置出站消息压缩器 func WithDecompressor(grpc.Decompressor) DialOption // 旧设置入站消息解压器 func UseCompressor(name) CallOption // 新按名字指定压缩器WithCompressor/WithDecompressor的源码定义位于 dialoptions.go注释均标注Deprecated: use encoding.RegisterCompressor instead. Will be supported throughout 1.x.即 1.x 版本周期内仍会得到支持。WithDecompressor的注释还揭示了其与注册表的关系入站响应消息的编码若与WithDecompressor的解压器Type()匹配则优先使用它否则按消息编码在encoding.RegisterCompressor注册表中查找若仍未找到返回Unimplemented状态错误。**出站请求发送方向**的判定顺序如下若使用了UseCompressor(name)消息按该名字对应的压缩器压缩若该名字未注册RPC 发送前即返回Internal错误若为UseCompressor(identity)不做压缩但仍会在头部发送identity给服务端。若使用了WithCompressor消息用该压缩器实现压缩。否则出站消息不压缩。**入站响应接收方向**的判定顺序如下若WithDecompressor设置的解压器与消息的编码匹配则使用它。若注册表中有与响应编码匹配的压缩器则使用它。否则流被关闭并向应用返回Unimplemented状态错误。服务端旧式 APIfunc RPCCompressor(grpc.Compressor) ServerOption // 旧压缩所有出站响应 func RPCDecompressor(grpc.Decompressor) ServerOption // 旧解压入站请求两者的源码定义位于 server.go均返回ServerOption。其中RPCDecompressor的注释与文档一致它比通过encoding.RegisterCompressor注册的解压器拥有更高优先级。**入站请求接收方向**的判定顺序如下若RPCDecompressor被使用且与请求编码匹配则使用它。若注册表中有与请求编码匹配的压缩器则使用它。否则向客户端返回Unimplemented状态。**出站响应发送方向**的判定顺序如下若RPCCompressor被使用所有响应消息都用该压缩器压缩。若入站请求使用了压缩且注册表中有对应压缩器则响应用同样的压缩方法。否则出站响应不压缩。需要说明的是服务端“请求用 gzip、响应也回 gzip”的行为在 server.go 的源码中有直接实现只有在未显式设置s.opts.cp即旧式RPCCompressor时才沿用入站请求的压缩方法一旦显式设置了RPCCompressor则全部响应一律使用该压缩器。这与文档中第 1、2 条规则的顺序完全一致。运行示例本地验证压缩链路在仓库根目录依次执行先起服务端再起客户端# 终端 1启动 echo 服务默认监听 :50051 go run ./examples/features/compression/server # 终端 2发起压缩 RPC go run ./examples/features/compression/client客户端默认连接localhost:50051服务端默认监听50051端口两者都可通过 flag 覆盖客户端-addr、服务端-port。客户端使用 gzip 发送compress消息服务端打印收到的消息后原样返回客户端校验返回内容与发送一致后正常退出# 服务端输出 server listening at [::]:50051 UnaryEcho called with message compress # 客户端输出 UnaryEcho call returned compress, nil验证要点服务端仅通过空导入注册 gzip没有为任何 RPC 显式设置压缩——压缩协商完全自动完成若删除客户端的grpc.UseCompressor(gzip.Name)RPC 将退化为不压缩但行为不变说明压缩是透明可选能力若在服务端移除 gzip 的空导入而客户端仍请求 gzip客户端将收到Unimplemented错误可借此观察前文所述的错误语义。小结grpc-go 的消息压缩体系可以概括为“一次注册、两处协商”客户端和服务端各自或同时通过encoding.RegisterCompressor注册压缩算法实现客户端用UseCompressor或WithDefaultCallOptions固化默认按名字选择出站压缩服务端自动按grpc-encoding头完成请求解压与同算法响应回压。压缩器缺失时客户端侧报Internal、服务端侧报Unimplemented方向不可混淆。对于存量代码中仍在使用的WithCompressor、WithDecompressor、RPCCompressor、RPCDecompressor等旧式 API官方已标记废弃但承诺在 1.x 周期内继续支持其与新版 API 的优先级规则已在上述新旧对照中给出可作为平滑迁移的依据。若需要自定义压缩算法encoding/gzip/gzip.go 中基于sync.Pool的实现是一个可直接套用的高质量模板。【免费下载链接】grpc-goThe Go language implementation of gRPC. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考