Files
zhuangang/topics/backend/grpc-microservices/01-grpc-proto.md
T

115 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.grpc-microservices.grpc-proto
title: gRPC 和 Protobuf:IDL、四种调用方式、错误码、超时、拦截器
type: lesson
domain: backend
stack: [grpc-microservices, go]
tags: [gRPC, Protobuf, HTTP/2, 拦截器, deadline]
level: intermediate
prerequisites: [backend.go.errors-concurrency]
est_hours: 3
related: [backend.grpc-microservices.governance, backend.grpc-microservices.interview]
week: [14]
order: 1
---
# gRPC 和 Protobuf:IDL、四种调用方式、错误码、超时、拦截器
## 解释说明
- **gRPC**:基于 HTTP/2 的 RPC 框架。先用 **Protobuf** 写接口定义(`.proto`),再由工具生成各语言的客户端和服务端代码,调用远程方法像调用本地函数。
- **Protobuf**:二进制序列化格式。比 JSON 体积小、解析快,有强类型约束;缺点是不可读,浏览器不能直接调用(需要 gRPC-Web 或网关转 HTTP/JSON)。
- **字段编号**:序列化时用编号而不是字段名。**已发布的编号不能改、不能复用**;删除字段时用 `reserved` 保留编号和名字。新增字段是向后兼容的。
- **四种调用方式**:一元(一问一答)、服务端流(一次请求、多次返回,例如推送进度)、客户端流(多次上传、一次返回,例如上传分片)、双向流(聊天、实时协作)。
- **错误码**:gRPC 有自己的状态码,常用 `InvalidArgument`、`NotFound`、`AlreadyExists`、`PermissionDenied`、`Unauthenticated`、`DeadlineExceeded`、`Unavailable`、`Internal`。可重试的一般只有 `Unavailable`(以及部分 `DeadlineExceeded`、`ResourceExhausted`),由业务判断。
- **Deadline(截止时间)**:客户端每次调用都要设置;截止时间会随请求传到服务端,服务端的 `ctx` 到期后应该停止工作,并继续传给下游调用。
- **拦截器(interceptor)**:gRPC 的中间件,用来做日志、鉴权、指标、恢复 panic。元数据(metadata)相当于 HTTP 头,用来传 token、请求 ID。
**和 REST 怎么选**:对外开放的接口、浏览器直接调用用 REST/JSON;内部服务之间、对性能和强类型契约有要求时用 gRPC。
## 案例
订单服务需要查用户信息:定义 `UserService.GetUser`,用户服务实现,订单服务通过生成的客户端调用,每次调用带 300ms 截止时间;用户不存在返回 `NotFound`,订单服务据此返回业务错误,而不是 500。
## 代码示例
接口定义([examples/grpc-user/user.proto](examples/grpc-user/user.proto)):
```proto
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User); // 服务端流
}
message GetUserRequest { int64 id = 1; }
message ListUsersRequest { int32 page_size = 1; }
message User {
int64 id = 1;
string name = 2;
string email = 3;
reserved 4; // 已删除字段的编号,不能复用
reserved "phone";
}
```
Go 服务端和客户端的关键写法(需要先用 `protoc --go_out --go-grpc_out` 生成 `userv1` 包;安装和生成命令以官方 Go 快速开始为准:https://grpc.io/docs/languages/go/quickstart/ ):
```go
// 服务端:实现生成的接口
type userServer struct {
userv1.UnimplementedUserServiceServer // 嵌入它,接口新增方法时不会编译失败
repo UserRepo
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
if req.GetId() <= 0 {
return nil, status.Error(codes.InvalidArgument, "id 必须大于 0")
}
u, err := s.repo.Find(ctx, req.GetId()) // 把 ctx 继续往下传,截止时间才能生效
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "用户 %d 不存在", req.GetId())
}
if err != nil {
return nil, status.Error(codes.Internal, "查询失败") // 不把内部错误细节暴露给调用方
}
return &userv1.User{Id: u.ID, Name: u.Name, Email: u.Email}, nil
}
// 一元拦截器:记录方法、耗时和状态码
func logging(ctx context.Context, req any, info *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := h(ctx, req)
log.Printf("%s %s %v", info.FullMethod, status.Code(err), time.Since(start))
return resp, err
}
// 客户端:每次调用都带截止时间
func getUser(cli userv1.UserServiceClient, id int64) (*userv1.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
u, err := cli.GetUser(ctx, &userv1.GetUserRequest{Id: id})
if status.Code(err) == codes.NotFound {
return nil, ErrUserNotFound
}
return u, err
}
```
可直接运行的完整演示用 Python(不需要单独安装 protoc):[examples/grpc-user/demo.py](examples/grpc-user/demo.py),覆盖一元调用、服务端流、`NOT_FOUND` 和 `DEADLINE_EXCEEDED`。
## 易错点
- **修改或复用字段编号**:老客户端解析出错乱的数据,而且不会报错。
- **客户端不设截止时间**:下游卡住时请求无限等待,连接和 goroutine 堆积。
- **服务端不把 `ctx` 传给下游**:客户端已经放弃了,服务端还在查数据库。
- **所有错误都返回 `Unknown` / `Internal`**:调用方无法区分“参数错”和“服务挂了”,也就无法决定是否重试。
- **每次调用都新建连接**:gRPC 连接(`grpc.ClientConn`)应该复用,它内部会管理 HTTP/2 连接。
- **把 proto 文件复制到各个仓库**:容易不一致,放在统一的仓库或用 Buf 等工具管理版本。
## 面试常问
见 [gRPC 与微服务面试题](interview.md):gRPC 和 REST 的区别、为什么快、四种调用方式、字段编号规则、deadline 传递。