第3章 gRPC入门:从零搭建你的第一个服务
好,咱们进入正题。gRPC 这东西,说白了就是 Google 搞出来的一个远程调用框架。我最早接触它是在 2017 年,当时公司要从单体拆微服务,选型时在 REST 和 gRPC 之间纠结了很久。后来我拍板用了 gRPC,原因很简单——性能好,而且强类型接口不容易扯皮。
3.1 gRPC 到底是什么?
gRPC 的全称是 gRPC Remote Procedure Calls。嗯,你没看错,名字里就有 RPC。它基于 HTTP/2 协议,默认用 Protocol Buffers(简称 protobuf)做序列化。
我打个比方你就懂了。REST 接口就像两个人写信——内容用 JSON 写,信封是 HTTP/1.1。而 gRPC 呢,像两个人直接打电话——内容用 protobuf 压缩,线路是 HTTP/2 的多路复用。你说哪个快?
核心特点:
- 基于 HTTP/2:支持双向流、多路复用、头部压缩
- 默认 protobuf:二进制序列化,比 JSON 小 3-10 倍
- 强类型接口:.proto 文件就是契约,谁也别耍赖
- 多语言支持:Java、Go、Python、C++ 等都能互通
我记得有一次,团队里新来的同事问我:“为什么不用 REST?大家都熟啊。” 我反问他:“你想想看,如果服务间调用一天几百万次,JSON 解析的开销你扛得住吗?” 他沉默了。后来我们压测,gRPC 比 REST 快了将近 40%。
3.2 Protocol Buffers 语法速览
protobuf 是 gRPC 的基石。你得先学会怎么写 .proto 文件。别怕,语法很简单,我带你过一遍。
3.2.1 定义一个消息
syntax = "proto3";
package user_service;
message UserRequest {
int32 user_id = 1;
string name = 2;
string email = 3;
}
message UserResponse {
int32 code = 1;
string message = 2;
User data = 3;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
看到没?每个字段后面有个数字,那是字段编号,1 到 15 占 1 个字节,16 以上占 2 个字节。我建议你把高频字段放在前面,能省带宽。
我的习惯:字段编号 1-15 留给必填字段,16-30 留给可选字段。这样后期加字段时,不会打乱已有编号。
3.2.2 字段类型与规则
| protobuf 类型 | 对应 Java 类型 | 对应 Go 类型 | 说明 |
|---|---|---|---|
| double | double | float64 | 64位浮点 |
| float | float | float32 | 32位浮点 |
| int32 | int | int32 | 变长编码,负数效率低 |
| string | String | string | UTF-8 编码 |
| bool | boolean | bool | 布尔值 |
| repeated | List | []T | 数组/列表 |
注意:protobuf 的 int32 用变长编码。如果你传负数,它会用 10 个字节!我曾经踩过这个坑,后来全改成 sint32 了。
3.3 定义 Service 与 Message
消息定义好了,接下来就是服务。gRPC 的服务定义,说白了就是声明有哪些 RPC 方法。
service UserService {
// 一元 RPC:客户端发一个请求,服务端回一个响应
rpc GetUser (UserRequest) returns (UserResponse);
// 服务端流式 RPC:客户端发一个请求,服务端不断返回数据
rpc ListUsers (UserRequest) returns (stream UserResponse);
// 客户端流式 RPC:客户端不断发数据,服务端最后回一个响应
rpc CreateUsers (stream UserRequest) returns (UserResponse);
// 双向流式 RPC:两边同时发,同时收
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
四种模式,你记一下。我项目中用得最多的是第一种和第四种。一元 RPC 适合查询,双向流适合实时通信,比如聊天或者日志推送。
为什么会用到流式?你想想看,如果我要返回 10 万条用户数据,用一元 RPC 的话,内存直接爆了。流式 RPC 可以一条一条发,客户端边收边处理,这才是正道。
3.4 生成客户端/服务端代码
写好了 .proto 文件,接下来就是生成代码。你需要安装 protoc 编译器,还有对应语言的插件。
3.4.1 安装工具
# 安装 protoc
brew install protobuf # macOS
apt install protobuf-compiler # Ubuntu
# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装 Java 插件(在 pom.xml 中添加)
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.54.0</version>
</dependency>
3.4.2 生成代码
# Go 示例
protoc --go_out=. --go-grpc_out=. user.proto
# Java 示例
protoc --java_out=. --grpc-java_out=. user.proto
执行完,你会看到生成了两个文件:
user.pb.go:消息的序列化/反序列化代码user_grpc.pb.go:gRPC 服务端和客户端的桩代码
避坑指南:我曾经因为 protoc 版本和插件版本不匹配,折腾了整整一下午。建议你统一用最新稳定版。另外,生成代码后不要手动修改,否则下次生成就覆盖了。
3.4.3 实现服务端
// Go 服务端示例
type userServer struct {
pb.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
// 模拟查询数据库
user := &pb.User{
Id: req.UserId,
Name: "张三",
Email: "zhangsan@example.com",
Age: 28,
}
return &pb.UserResponse{
Code: 0,
Message: "success",
Data: user,
}, nil
}
func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &userServer{})
s.Serve(lis)
}
3.4.4 编写客户端
// Go 客户端示例
func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := pb.NewUserServiceClient(conn)
resp, _ := client.GetUser(context.Background(), &pb.UserRequest{
UserId: 1001,
})
fmt.Printf("用户信息: %+v\n", resp.Data)
}
看到没?客户端调用就像调用本地函数一样。你传一个 UserRequest,它返回一个 UserResponse。这就是 RPC 的魅力——屏蔽了网络细节。
3.5 本章小结
嗯,这一章内容不少,但都是基础。我帮你捋一下重点:
- gRPC 基于 HTTP/2 + protobuf,性能碾压 REST
- .proto 文件是核心,字段编号要规划好
- 四种 RPC 模式:一元、服务端流、客户端流、双向流
- 生成代码后,服务端实现接口,客户端调用接口
下一章我会带你实战一个完整的用户服务,包括错误处理、超时控制和拦截器。到时候你会看到,gRPC 在真实项目中到底有多香。
对了,如果你在生成代码时遇到问题,记得检查 protoc 版本。我当年就是被版本坑过,后来学乖了,每次都用 protoc --version 确认一下。