强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

HTTP/2 与 RPC 精讲教程 / 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: "alice@example.com", Status: pb.UserStatus_USER_STATUS_ACTIVE},
			2: {Id: 2, Name: "Bob", Email: "bob@example.com", 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 包中的通用类型

8.9 扩展阅读


第 07 章 - HTTP/3 与 QUIC | 第 09 章 - gRPC 流式通信