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