第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 确认一下。