第4章:gRPC 通信基础

好,咱们进入微服务通信的重头戏——gRPC。说实话,我在做微服务拆分时,最头疼的就是服务之间怎么说话。RESTful API 用久了,总觉得有点「笨重」。后来接触到 gRPC,嗯,真香。

4.1 gRPC 简介

gRPC 是 Google 开源的高性能 RPC 框架。它基于 HTTP/2 协议,默认用 Protobuf 做序列化。说白了,就是让两个服务像调用本地方法一样通信。

我刚开始用 gRPC 时,有个直觉:它比 REST 快。后来压测发现,同样业务场景下,gRPC 的吞吐量能高出 3-5 倍。为什么?因为 Protobuf 是二进制格式,比 JSON 小太多了。

gRPC 的核心优势:

  • 高性能:二进制序列化 + HTTP/2 多路复用
  • 强类型:接口定义即契约,不会出现字段对不上的问题
  • 双向流:服务端和客户端可以同时发送数据
  • 多语言支持:.NET、Java、Go、Python 都能互通

我记得有个项目,团队用 REST 接口传大对象,每次序列化都要几十毫秒。换成 gRPC 后,延迟直接降到个位数。你想想看,这对高并发场景意味着什么。

4.2 Protobuf 语法

Protobuf 是 gRPC 的「语言」。你得先定义好 .proto 文件,然后生成代码。我习惯把 .proto 文件放在单独的项目里,方便共享。

4.2.1 基本语法

先看一个最简单的例子:

syntax = "proto3";

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

这里要注意几点:

  • syntax = "proto3" 必须写在第一行
  • 每个字段后面有个数字,那是字段编号,不能重复
  • service 定义 RPC 方法

我的习惯:字段编号从 1 开始,预留一些编号给未来可能新增的字段。比如 1-10 给核心字段,11-20 给扩展字段。这样以后加字段不会破坏兼容性。

4.2.2 常用数据类型

Protobuf 类型 C# 对应类型 说明
int32 int 32位整数
int64 long 64位整数
float float 32位浮点数
double double 64位浮点数
string string UTF-8 字符串
bool bool 布尔值
bytes ByteString 二进制数据
repeated List<T> 数组/列表

我曾经踩过一个坑:用 int32 存时间戳。后来发现 2038 年问题,赶紧改成 int64。嗯,设计 .proto 文件时,时间戳最好用 google.protobuf.Timestamp 类型。

4.2.3 嵌套消息与枚举

message Order {
  int32 id = 1;
  string customer_name = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;

  message OrderItem {
    string product_name = 1;
    int32 quantity = 2;
    double price = 3;
  }

  enum OrderStatus {
    PENDING = 0;
    PROCESSING = 1;
    COMPLETED = 2;
    CANCELLED = 3;
  }
}

嵌套消息让结构更清晰。我一般把关联紧密的字段放在一起,比如订单和订单项。枚举值从 0 开始,这是 Protobuf 的约定。

4.3 .NET Core 中 gRPC 服务端搭建

好,理论讲完了,咱们动手。在 .NET Core 里搭建 gRPC 服务端,其实很简单。

4.3.1 创建项目

dotnet new grpc -n GrpcServer
cd GrpcServer

这个模板会自动生成一个示例项目。你会看到 Protos 文件夹,里面有个 greet.proto 文件。我建议你删掉它,换成自己的 .proto 文件。

4.3.2 配置 .proto 文件

把刚才的 Greeter 服务定义保存为 Protos/greeter.proto。然后在 .csproj 文件里加上:

<ItemGroup>
  <Protobuf Include="Protos\greeter.proto" GrpcServices="Server" />
</ItemGroup>

这里 GrpcServices="Server" 表示只生成服务端代码。客户端项目用 Client

4.3.3 实现服务

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloResponse> SayHello(HelloRequest request, ServerCallContext context)
    {
        _logger.LogInformation($"收到请求: {request.Name}");

        return Task.FromResult(new HelloResponse
        {
            Message = $"你好, {request.Name}!"
        });
    }
}

注意,这里继承的是自动生成的 Greeter.GreeterBase 类。方法名和 .proto 文件里定义的要一致。

我曾经犯过的错:在服务方法里用了 async/await,但忘了返回 Task。结果客户端一直超时。记住,gRPC 服务方法必须返回 Task<T> 或者 AsyncUnaryCall<T>。

4.3.4 注册服务

在 Program.cs 里:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.MapGrpcService<GreeterService>();

app.Run();

就这么简单。启动后,gRPC 服务就运行在默认端口 5000 上了。

4.4 .NET Core 中 gRPC 客户端搭建

服务端搭好了,客户端怎么调用?

4.4.1 创建客户端项目

dotnet new console -n GrpcClient
cd GrpcClient

4.4.2 添加引用

在 .csproj 里加上:

<ItemGroup>
  <Protobuf Include="..\GrpcServer\Protos\greeter.proto" GrpcServices="Client" />
  <PackageReference Include="Google.Protobuf" Version="3.21.0" />
  <PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
  <PackageReference Include="Grpc.Tools" Version="2.49.0" />
</ItemGroup>

这里引用了服务端的 .proto 文件,但 GrpcServices="Client" 表示生成客户端代码。

4.4.3 调用服务

using Grpc.Net.Client;
using GrpcClient;

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest
{
    Name = "小明"
});

Console.WriteLine(response.Message);

你看,调用远程服务就像调用本地方法一样。这就是 RPC 的魅力。

我的建议:生产环境里,客户端不要每次都创建新的 GrpcChannel。复用 channel 能提高性能。我一般用依赖注入把 channel 注册为单例。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 证书问题:开发环境用 HTTP,生产环境必须用 HTTPS。gRPC 默认要求加密连接。
  • 超时设置:客户端调用时记得设置超时,不然服务端挂了客户端会一直等。
  • 字段编号:一旦发布,不要修改已有字段的编号。否则旧客户端会解析错误。
  • 流式调用:如果数据量大,考虑用服务端流或双向流,别一股脑全塞进一个请求里。

我记得有一次,线上服务突然报错,查了半天发现是 .proto 文件里加了个新字段,但编号和旧字段冲突了。从那以后,我每次修改 .proto 文件都会仔细检查编号。

好了,gRPC 的基础就讲到这里。下一章咱们聊聊更高级的通信模式——流式调用和拦截器。你想想看,如果服务端能主动推送数据,是不是比轮询优雅多了?