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

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/JSONgRPC/Protobuf优势倍数
序列化大小文本,较大二进制,紧凑2-10x 更小
序列化速度慢(JSON 解析)快(二进制编解码)5-10x 更快
类型安全弱(依赖文档)强(IDL 定义)编译时检查
流式传输不原生支持四种模式-
代码生成手动/第三方官方支持多语言一致
传输协议HTTP/1.1 或 HTTP/2HTTP/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 包中的通用类型

8.9 扩展阅读


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