gRPC 微服务通信:Protocol Buffers 与流式调用
什么是 gRPC?
gRPC 是一个高性能、开源的远程过程调用(RPC)框架,最初由 Google 开发。它允许客户端程序像调用本地方法一样直接调用不同机器上的服务端应用,非常适合微服务架构下的服务间通信。
与 RESTful API 不同,gRPC 的核心优势在于:
- 使用 HTTP/2 协议:支持多路复用、头部压缩、服务器推送,大幅降低延迟。
- 强接口定义:通过 Protocol Buffers 定义服务契约,保证客户端和服务端严格一致。
- 多语言支持:官方支持 C++、Java、Go、Python、Node.js 等十多种语言。
- 内置流式通信:轻松实现客户端流、服务端流和双向流。
理解 Protocol Buffers
Protocol Buffers(protobuf)是一种与语言无关、平台无关的序列化结构数据的机制。在 gRPC 中,它负责:
- 定义服务(service)和消息(message)的结构。
- 自动生成客户端和服务端的代码桩(stub)。
- 高效地序列化/反序列化二进制数据,比 JSON 更小、更快。
一个典型的 .proto 文件如下:
syntax = "proto3";
package ecommerce;
// 定义商品消息
message Product {
int32 id = 1;
string name = 2;
double price = 3;
}
// 定义请求和响应消息
message SearchRequest {
string keyword = 1;
}
message SearchResponse {
repeated Product products = 1;
}
// 定义服务
service ProductService {
rpc SearchProducts(SearchRequest) returns (SearchResponse);
}
每条字段后面的数字是“字段标签”,用于二进制格式中的字段标识,一旦定义就不能随意更改,以保证兼容性。
安装编译器与代码生成
你需要安装 protoc 编译器和对应语言的 gRPC 插件。以 Go 为例:
# 安装 protoc
# 从 https://github.com/protocolbuffers/protobuf/releases 下载
# 安装 Go 的 gRPC 和 protobuf 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
之后运行命令生成代码:
protoc --go_out=. --go-grpc_out=. product.proto
生成的代码中包含消息的结构体和服务端/客户端的接口,你只需实现业务逻辑。
构建第一个 gRPC 服务(一元 RPC)
一元 RPC 是最简单的模式:客户端发送一个请求,服务端返回一个响应,就像普通的函数调用。
服务端实现(Go)
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "yourmodule/product"
)
type productServer struct {
pb.UnimplementedProductServiceServer
}
func (s *productServer) SearchProducts(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) {
// 模拟检索逻辑
products := []*pb.Product{
{Id: 1, Name: "Laptop", Price: 999.99},
}
return &pb.SearchResponse{Products: products}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterProductServiceServer(s, &productServer{})
log.Println("Server listening on port 50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
客户端调用(Go)
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "yourmodule/product"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewProductServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := client.SearchProducts(ctx, &pb.SearchRequest{Keyword: "laptop"})
if err != nil {
log.Fatalf("could not search: %v", err)
}
log.Printf("Found products: %v", resp.Products)
}
这就是一次完整的 gRPC 调用。但 gRPC 真正的威力在流式通信。
流式调用深入解析
gRPC 支持四种服务方法类型,除一元 RPC 外,还有三种流式模式。流式调用允许一方或双方持续发送数据,在实时推送、大文件传输、消息队列等场景极为有用。
服务端流式 RPC
客户端发送一次请求,服务端返回一个流,持续发送一系列消息。
场景:实时日志推送、股票行情推送。
修改 .proto 文件,在响应类型前加 stream:
service LogService {
rpc StreamLogs(LogRequest) returns (stream LogEntry);
}
message LogRequest {
string level = 1;
}
message LogEntry {
string message = 1;
}
服务端实现核心代码:
func (s *logServer) StreamLogs(req *pb.LogRequest, stream pb.LogService_StreamLogsServer) error {
for i := 0; i < 10; i++ {
entry := &pb.LogEntry{Message: fmt.Sprintf("Log line %d", i)}
if err := stream.Send(entry); err != nil {
return err
}
time.Sleep(1 * time.Second)
}
return nil
}
客户端接收数据时,使用循环读取流:
stream, err := client.StreamLogs(ctx, &pb.LogRequest{Level: "INFO"})
for {
entry, err := stream.Recv()
if err == io.EOF {
break
}
// 处理每一条日志
}
客户端流式 RPC
客户端发送数据流,服务端在接收完所有数据后返回一个响应。适用于文件上传、传感器数据批量提交。
在 proto 中定义:
service FileService {
rpc Upload(stream FileChunk) returns (UploadStatus);
}
message FileChunk {
bytes data = 1;
}
message UploadStatus {
bool success = 1;
string message = 2;
}
服务端在方法中通过 stream.Recv() 循环接收数据,直到 io.EOF 结束:
func (s *fileServer) Upload(stream pb.FileService_UploadServer) error {
var totalBytes int64
for {
chunk, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&pb.UploadStatus{Success: true, Message: fmt.Sprintf("Received %d bytes", totalBytes)})
}
if err != nil {
return err
}
totalBytes += int64(len(chunk.Data))
}
}
客户端发送流:
stream, _ := client.Upload(ctx)
for _, chunk := range fileChunks {
stream.Send(chunk)
}
resp, err := stream.CloseAndRecv() // 关闭发送并获取响应
双向流式 RPC
双方可以独立、异步地读写流,顺序灵活,适用于聊天、交互式协作、实时对战游戏等。在 proto 中请求和响应都用 stream 修饰:
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user = 1;
string text = 2;
}
服务端实现:同时处理读和写,通常需要两个 goroutine。
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// 广播或回复
stream.Send(&pb.ChatMessage{User: "Server", Text: "Received: " + msg.Text})
}
}
客户端:
stream, _ := client.Chat(ctx)
// 发送协程
go func() {
for _, msg := range messages {
stream.Send(msg)
}
stream.CloseSend()
}()
// 接收循环
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
fmt.Println(msg.Text)
}
生产环境注意事项
- TLS 加密:生产环境务必启用 TLS,避免明文传输。gRPC 原生支持 SSL/TLS。
- 服务发现与负载均衡:可结合 etcd、Consul 或 Kubernetes DNS,客户端需要配置负载均衡策略(如 Round Robin)。
- 超时与重试:使用 context 设置 deadline,部分中间件支持自动重试。
- 拦截器(Interceptor):类似中间件,用于日志、认证、监控等。
- 性能优化:连接复用、流控(flow control)、适当调整消息大小。
与 REST 的对比与选型
| 特性 | gRPC | REST (JSON over HTTP) |
|---|---|---|
| 数据格式 | Protobuf(二进制) | JSON(文本) |
| 传输效率 | 更高(体积小、解析快) | 较低 |
| 接口契约 | 严格的 .proto 定义 | 非强制性(OpenAPI) |
| 流式支持 | 原生双向流 | 需要 WebSocket 或 SSE |
| 浏览器支持 | 需 gRPC-Web 转换 | 原生支持 |
| 调试便利性 | 需专用工具(grpcurl) | 可直接 curl 测试 |
如果微服务之间对性能要求高、需要强契约和流式场景,gRPC 是理想选择;对外暴露 API 给外部调用时,REST 或 GraphQL 可能更合适。
总结
本教程介绍了 gRPC 微服务通信的核心概念:Protocol Buffers 的数据定义和代码生成,以及四种调用模式的具体实现。通过实际代码示例,你应该已经掌握如何搭建一个基本的 gRPC 服务,并运用流式调用解决实际场景需求。下一步建议动手练习,将示例中的产品服务改造为流式版本,或尝试在不同语言间进行互操作。