08 - gRPC 基础
第 08 章:gRPC 基础
Protocol Buffers + HTTP/2 = 现代 RPC 的黄金组合
8.1 gRPC 概述
gRPC(Google Remote Procedure Call)是 Google 开发的高性能、开源的 RPC 框架。它使用 Protocol Buffers 作为接口定义语言(IDL)和序列化协议,底层基于 HTTP/2 进行传输。
8.1.1 为什么选择 gRPC
| 维度 | REST/JSON | gRPC/Protobuf | 优势倍数 |
|---|---|---|---|
| 序列化大小 | 文本,较大 | 二进制,紧凑 | 2-10x 更小 |
| 序列化速度 | 慢(JSON 解析) | 快(二进制编解码) | 5-10x 更快 |
| 类型安全 | 弱(依赖文档) | 强(IDL 定义) | 编译时检查 |
| 流式传输 | 不原生支持 | 四种模式 | - |
| 代码生成 | 手动/第三方 | 官方支持 | 多语言一致 |
| 传输协议 | HTTP/1.1 或 HTTP/2 | HTTP/2 | 多路复用 |
8.1.2 gRPC 的核心特性
gRPC 的技术栈:
┌──────────────────────────────────────────┐
│ 应用代码 (Generated) │
├──────────────────────────────────────────┤
│ gRPC Stub (自动生成) │
│ ┌──────────────────────────────────┐ │
│ │ Client Stub │ Server Skeleton │ │
│ └──────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ Protocol Buffers (序列化/反序列化) │
├──────────────────────────────────────────┤
│ HTTP/2 (传输层) │
├──────────────────────────────────────────┤
│ TCP/TLS │
└──────────────────────────────────────────┘
8.2 Protocol Buffers
8.2.1 Protobuf 简介
Protocol Buffers(简称 Protobuf)是 Google 开发的语言无关、平台无关的序列化机制。
// user.proto
syntax = "proto3";
package example;
option go_package = "example/pb";
// 用户消息定义
message User {
int64 id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
repeated string roles = 5;
map<string, string> metadata = 6;
}
enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
USER_STATUS_BANNED = 3;
}
// 请求/响应消息
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
int32 total_count = 3;
}
8.2.2 Protobuf 数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
int32, int64 | 有符号整数 | int64 id = 1; |
uint32, uint64 | 无符号整数 | uint64 count = 2; |
float, double | 浮点数 | double price = 3; |
bool | 布尔 | bool active = 4; |
string | 字符串 | string name = 5; |
bytes | 二进制 | bytes data = 6; |
enum | 枚举 | enum Status { ... } |
message | 嵌套消息 | Address addr = 7; |
repeated | 列表/数组 | repeated string tags = 8; |
map | 键值对 | map<string, string> labels = 9; |
oneof | 联合体 | oneof result { ... } |
optional | 可选字段 | optional int32 age = 10; |
8.2.3 服务定义
// service.proto
syntax = "proto3";
package example;
import "user.proto";
// 用户服务定义
service UserService {
// 一元 RPC(Unary)
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// 服务端流式 RPC
rpc ListUsers(ListUsersRequest) returns (stream User);
// 客户端流式 RPC
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateResponse);
// 双向流式 RPC
rpc WatchUsers(WatchRequest) returns (stream UserEvent);
}
// 辅助消息
message CreateUserRequest {
string name = 1;
string email = 2;
}
message BatchCreateResponse {
repeated User users = 1;
int32 created_count = 2;
}
message WatchRequest {
repeated int64 user_ids = 1;
}
message UserEvent {
enum EventType {
EVENT_TYPE_UNSPECIFIED = 0;
EVENT_TYPE_CREATED = 1;
EVENT_TYPE_UPDATED = 2;
EVENT_TYPE_DELETED = 3;
}
EventType type = 1;
User user = 2;
int64 timestamp = 3;
}
8.3 gRPC 四种通信模式
8.3.1 模式概览
| 模式 | 请求 | 响应 | 典型场景 |
|---|---|---|---|
| 一元(Unary) | 单个 | 单个 | 查询用户、创建订单 |
| 服务端流(Server Streaming) | 单个 | 流式 | 列表查询、日志推送 |
| 客户端流(Client Streaming) | 流式 | 单个 | 批量上传、文件上传 |
| 双向流(Bidirectional) | 流式 | 流式 | 实时通信、聊天 |
8.3.2 一元 RPC(Unary RPC)
客户端 服务器
|--- Request --------->|
| | 处理请求
|<-------- Response ----|
特点:
- 最简单的模式
- 类似 HTTP 的请求-响应模型
- 每次调用一个 HTTP/2 请求
// 一元 RPC 服务端实现
package main
import (
"context"
"log"
"net"
pb "example/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type userService struct {
pb.UnimplementedUserServiceServer
users map[int64]*pb.User
}
func (s *userService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "用户 %d 不存在", req.Id)
}
return &pb.GetUserResponse{User: user}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
server := grpc.NewServer()
pb.RegisterUserServiceServer(server, &userService{
users: map[int64]*pb.User{
1: {Id: 1, Name: "Alice", Email: "[email protected]", Status: pb.UserStatus_USER_STATUS_ACTIVE},
2: {Id: 2, Name: "Bob", Email: "[email protected]", Status: pb.UserStatus_USER_STATUS_ACTIVE},
},
})
log.Println("gRPC 服务器启动于 :50051")
if err := server.Serve(lis); err != nil {
log.Fatalf("服务失败: %v", err)
}
}
// 一元 RPC 客户端
package main
import (
"context"
"fmt"
"log"
"time"
pb "example/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
log.Fatalf("调用失败: %v", err)
}
fmt.Printf("用户: %s (%s)\n", resp.User.Name, resp.User.Email)
}
8.4 Protocol Buffers 编码原理
8.4.1 编码格式
每个字段编码为:Tag + Value
Tag = (field_number << 3) | wire_type
Wire Types:
0 - Varint(int32, int64, bool, enum)
1 - 64-bit(double, fixed64)
2 - Length-delimited(string, bytes, 嵌套 message)
5 - 32-bit(float, fixed32)
示例:User { id: 1, name: "Alice" }
id=1, wire_type=0:
Tag: (1 << 3) | 0 = 0x08
Value: 0x01
name="Alice", wire_type=2:
Tag: (2 << 3) | 2 = 0x12
Length: 5
Value: 0x416C696365 (ASCII: Alice)
总计:1 + 1 + 1 + 1 + 5 = 9 字节
等价 JSON:{"id":1,"name":"Alice"} = 26 字节
Protobuf 节省 65%!
8.4.2 编码实现演示
def encode_varint(value: int) -> bytes:
"""编码 Varint"""
result = []
while value > 0:
byte = value & 0x7F
value >>= 7
if value > 0:
byte |= 0x80 # 设置继续位
result.append(byte)
return bytes(result)
def encode_field(field_number: int, wire_type: int, value) -> bytes:
"""编码字段"""
tag = (field_number << 3) | wire_type
result = bytes([tag])
if wire_type == 0: # Varint
result += encode_varint(value)
elif wire_type == 2: # Length-delimited
data = value.encode('utf-8') if isinstance(value, str) else value
result += encode_varint(len(data)) + data
return result
# 编码 User { id: 1, name: "Alice" }
encoded = b""
encoded += encode_field(1, 0, 1) # id = 1
encoded += encode_field(2, 2, "Alice") # name = "Alice"
print(f"Protobuf 编码: {encoded.hex()}")
print(f"编码大小: {len(encoded)} 字节")
# JSON 对比
import json
json_data = json.dumps({"id": 1, "name": "Alice"})
print(f"JSON 大小: {len(json_data)} 字节")
print(f"压缩率: {len(encoded)/len(json_data)*100:.1f}%")
8.5 代码生成
8.5.1 安装工具
# 安装 protoc(Protocol Buffers 编译器)
# Ubuntu/Debian
sudo apt-get install -y protobuf-compiler
# macOS
brew install protobuf
# 验证安装
protoc --version
# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装 Python 插件
pip install grpcio grpcio-tools
# 安装 Java 插件(Gradle/Maven 自动管理)
8.5.2 项目结构
project/
├── proto/
│ └── example/
│ ├── user.proto
│ └── service.proto
├── gen/
│ └── go/
│ └── example/
│ ├── user.pb.go (生成)
│ └── service_grpc.pb.go (生成)
├── cmd/
│ ├── server/
│ │ └── main.go
│ └── client/
│ └── main.go
└── go.mod
8.5.3 生成命令
# Go 代码生成
protoc \
--proto_path=proto \
--go_out=gen/go --go_opt=paths=source_relative \
--go-grpc_out=gen/go --go-grpc_opt=paths=source_relative \
proto/example/*.proto
# Python 代码生成
python -m grpc_tools.protoc \
--proto_path=proto \
--python_out=gen/python \
--grpc_python_out=gen/python \
proto/example/*.proto
# Java 代码生成
protoc \
--proto_path=proto \
--java_out=gen/java \
--grpc-java_out=gen/java \
proto/example/*.proto
8.5.4 Makefile 自动化
# Makefile
PROTO_DIR = proto
GEN_DIR = gen
.PHONY: proto
proto:
@echo "生成 gRPC 代码..."
protoc \
--proto_path=$(PROTO_DIR) \
--go_out=$(GEN_DIR)/go --go_opt=paths=source_relative \
--go-grpc_out=$(GEN_DIR)/go --go-grpc_opt=paths=source_relative \
$(PROTO_DIR)/example/*.proto
@echo "完成"
.PHONY: clean
clean:
rm -rf $(GEN_DIR)/*
.PHONY: lint
lint:
buf lint $(PROTO_DIR)
8.6 gRPC vs REST 对比实战
# 性能对比测试
import time
import json
import requests
# REST 方式
def rest_get_user(user_id: int):
start = time.time()
resp = requests.get(f"http://localhost:8080/api/users/{user_id}")
data = resp.json()
return time.time() - start, len(resp.content)
# gRPC 方式
import grpc
import user_pb2
import user_pb2_grpc
def grpc_get_user(user_id: int):
channel = grpc.insecure_channel('localhost:50051')
stub = user_pb2_grpc.UserServiceStub(channel)
start = time.time()
response = stub.GetUser(user_pb2.GetUserRequest(id=user_id))
data = response.SerializeToString()
return time.time() - start, len(data)
# 批量测试
print("=== 单次请求对比 ===")
rest_time, rest_size = rest_get_user(1)
grpc_time, grpc_size = grpc_get_user(1)
print(f"REST: {rest_time*1000:.2f}ms, {rest_size} bytes")
print(f"gRPC: {grpc_time*1000:.2f}ms, {grpc_size} bytes")
print(f"速度提升: {rest_time/grpc_time:.1f}x")
print(f"大小节省: {(1-grpc_size/rest_size)*100:.1f}%")
print("\n=== 1000 次请求对比 ===")
rest_total = sum(rest_get_user(i % 100 + 1)[0] for i in range(1000))
grpc_total = sum(grpc_get_user(i % 100 + 1)[0] for i in range(1000))
print(f"REST 总耗时: {rest_total:.2f}s ({1000/rest_total:.0f} QPS)")
print(f"gRPC 总耗时: {grpc_total:.2f}s ({1000/grpc_total:.0f} QPS)")
8.7 业务场景:微服务用户中心
场景:电商平台的用户中心微服务
服务划分:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order Service│ ──→ │ User Service│ ──→ │ Auth Service│
│ (gRPC Client)│ │ (gRPC Server│ │ (gRPC Server│
│ │ │ + Client) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
接口设计:
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc BatchGetUsers(BatchGetRequest) returns (BatchGetResponse);
rpc ValidateToken(ValidateTokenRequest) returns (ValidateResponse);
}
性能要求:
- 延迟 P99 < 10ms
- 吞吐量 > 10,000 QPS
- 可用性 99.99%
8.8 注意事项
⚠️ Protobuf 向后兼容:
- 新增字段使用新编号,不要复用已删除的编号
- 不要修改已有字段的类型或编号
- 使用
reserved标记已删除的字段编号
⚠️ 默认值陷阱:
- Proto3 中所有字段都有零值默认值
- 无法区分"未设置"和"设置为零值"
- 需要区分时使用
optional关键字或包装类型
⚠️ 错误处理:
- gRPC 使用状态码表示错误(不同于 HTTP 状态码)
- 使用
status.Error()创建结构化错误 - 传递错误详情使用
status.WithDetails()
💡 最佳实践:
- 服务定义放在独立的 Git 仓库中
- 使用 Buf 工具管理 Protobuf 依赖
- 合理使用
google.protobuf包中的通用类型