1. gRPC 与 Protobuf 概览
一句话总结: gRPC 基于 HTTP/2,用 Protobuf 做二进制序列化,契约先行、多语言互通,是微服务间内部调用的高效选择。
gRPC 由 Google 开源,默认传输为 HTTP/2,序列化为 Protocol Buffers(.proto 文件定义)。相比 REST/JSON,它在性能(二进制、头部压缩、多路复用)与契约严格性(强类型 schema)上更胜一筹,特别适合服务间高频、低延迟的调用。
// 一个最小的 Proto 契约
syntax = "proto3";
package order;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc CreateOrder (CreateOrderRequest) returns (Order);
}
message GetOrderRequest {
int32 id = 1;
}
message Order {
int32 id = 1;
string customer = 2;
double total = 3;
repeated OrderLine lines = 4;
}
message OrderLine {
int32 product_id = 1;
int32 quantity = 2;
}
| 特性 | gRPC | REST/JSON |
|---|---|---|
| 传输协议 | HTTP/2 | HTTP/1.1 为主 |
| 序列化 | Protobuf 二进制 | JSON 文本 |
| 契约 | .proto 强类型 | OpenAPI 文档 |
| 多路复用 | 支持 | 需连接池 |
| 适用 | 服务间调用 | 浏览器/开放 API |
避坑: gRPC 的客户端需要专门的 stub,浏览器默认不支持 HTTP/2 裸 gRPC。面向浏览器的场景要用 gRPC-Web 或继续用 REST。契约一旦发布,字段编号(
= 1)不能改动——那是二进制协议的兼容性根基。
2. Proto 契约与服务定义
一句话总结: .proto 是服务的唯一事实来源,message 字段编号、命名空间与 package 约定决定了跨语言的兼容性与代码组织。
定义 Proto 时,package 决定生成代码的命名空间,service 声明一组 RPC,message 声明数据结构。字段编号从 1 开始,删除字段要留编号占位(用 reserved),避免复用编号造成兼容性灾难。
syntax = "proto3";
package shop.orders.v1; // 生成命名空间 Shop.Orders.V1
import "google/protobuf/timestamp.proto";
message Order {
int32 id = 1;
string customer = 2;
double total = 3;
google.protobuf.Timestamp created_at = 4;
repeated OrderLine lines = 5;
reserved 6; // 已删除字段,编号不可复用
reserved "legacy_flag";
}
service OrderApi {
rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);
rpc GetOrder (GetOrderRequest) returns (Order);
rpc WatchOrders (WatchOrdersRequest) returns (stream Order); // 服务端流
}
| 规则 | 说明 |
|---|---|
| 字段编号 | 1~2^29-1,生成后不可改 |
| 删除字段 | reserved 占位防复用 |
| 版本策略 | 用 package 分层(v1/v2) |
| 可选字段 | optional 显式表达缺失 |
| 枚举 | 首个值必须为 0 |
避坑: 跨团队协作时,把
.proto当契约评审对象:字段语义、命名、编号分配都要走评审。接口变更优先「加字段 + 后向兼容」,破坏性变更才升版本。别在 message 里塞业务逻辑依赖的魔法编号,一切以 schema 为准。
3. 代码生成与服务实现
一句话总结: .NET 的 Grpc.Tools 在编译期从 .proto 生成强类型客户端与服务器基类,开发者只实现基类方法即可。
在 .csproj 里引用 Grpc.AspNetCore 与 Grpc.Tools,把 .proto 标记为 Protobuf item,编译时自动生成代码。服务器继承生成的 OrderApiBase,客户端使用生成的 OrderApiClient。
<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Both" />
</ItemGroup>
// Program.cs —— 注册 gRPC 服务
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<OrderApiService>();
app.Run();
// 服务实现:继承生成的基类
public class OrderApiService(IOrderRepository repo) : OrderApi.OrderApiBase
{
public override async Task<Order> GetOrder(GetOrderRequest request, ServerCallContext context)
{
var order = await repo.GetByIdAsync(request.Id, context.CancellationToken);
return order is null
? throw new RpcException(new Status(StatusCode.NotFound, "order not found"))
: MapToProto(order);
}
}
// 客户端调用
using var channel = GrpcChannel.ForAddress("https://orders.internal:5001");
var client = new OrderApi.OrderApiClient(channel);
var reply = await client.GetOrderAsync(new GetOrderRequest { Id = 42 });
Console.WriteLine(reply.Customer);
| 产物 | 说明 |
|---|---|
OrderApiBase | 服务器虚基类,override 实现 |
OrderApiClient | 强类型客户端 stub |
GrpcChannel | 客户端连接抽象 |
ServerCallContext | 携带元数据、取消、死线 |
避坑: 服务器方法里的异常要映射为
RpcException+StatusCode,而不是抛普通Exception——后者会成为Unknown,客户端丢失业务语义。ServerCallContext.CancellationToken必须传给一切底层调用,否则客户端取消后服务器还在跑。
4. 拦截器横切逻辑
一句话总结: 拦截器类似 ASP.NET Core 中间件,在请求前后统一处理认证、日志、指标与错误转换,避免在每个 RPC 里重复。
gRPC 拦截器分服务端与客户端两侧:服务端在方法执行前后介入,客户端在发请求前后介入。常用于:注入 token、记录耗时、统一异常转 RpcException、附加 traceId 元数据。
// 服务端拦截器:日志 + 耗时
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger) => _logger = logger;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request, ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var sw = Stopwatch.StartNew();
try
{
return await continuation(request, context);
}
catch (RpcException) { throw; }
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.Internal, ex.Message));
}
finally
{
sw.Stop();
_logger.LogInformation("gRPC {Method} took {ElapsedMs}ms",
context.Method, sw.ElapsedMilliseconds);
}
}
}
// 注册拦截器
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
| 拦截器用途 | 说明 |
|---|---|
| 认证 | 从 Metadata 读取 token,校验后附加 Claims |
| 日志 | 方法名、耗时、状态 |
| 错误转换 | 业务异常 → StatusCode |
| 指标 | 计数器 + 直方图 |
| 链路追踪 | 注入/读取 traceId |
避坑: 拦截器里别吞掉异常——至少要
throw,让业务语义传递下去。拦截器是横切逻辑的家,但不该承载业务规则。服务端拦截器拿不到[Authorize]特性那么细的声明式能力,认证逻辑要配合 gRPC 的认证中间件一起用。
5. 流式 RPC
一句话总结: gRPC 支持四种调用模式,流式通信让「大结果分批返回」与「双向持续对话」成为可能,适合实时推送与大数据传输。
四种模式:一元(Unary)、服务端流(Server streaming)、客户端流(Client streaming)、双向流(Bidirectional streaming)。服务端流适合批量导出,客户端流适合批量上传,双向流适合聊天、实时监控。
// 服务端流:边查边推
public override async Task WatchOrders(
WatchOrdersRequest request,
IServerStreamWriter<Order> responseStream,
ServerCallContext context)
{
await foreach (var order in _orderFeed.WatchAsync(context.CancellationToken))
{
await responseStream.WriteAsync(order); // 推送一条
}
}
// 双向流:客户端不断发、服务器不断回
public override async Task Chat(
IAsyncStreamReader<ChatMessage> requestStream,
IServerStreamWriter<ChatReply> responseStream,
ServerCallContext context)
{
await foreach (var msg in requestStream.ReadAllAsync(context.CancellationToken))
{
var reply = new ChatReply { Text = $"echo: {msg.Text}" };
await responseStream.WriteAsync(reply);
}
}
| 模式 | 客户端 | 服务端 | 场景 |
|---|---|---|---|
| 一元 | 1 请求 | 1 响应 | 常规查询 |
| 服务端流 | 1 请求 | N 响应 | 导出、推送 |
| 客户端流 | N 请求 | 1 响应 | 批量上传 |
| 双向流 | N 请求 | N 响应 | 聊天、实时 |
避坑: 流式响应要时刻关注背压:客户端处理慢时,
WriteAsync会阻塞或缓冲,内存随之增长。长连接要处理CancellationToken,客户端断开后及时释放。不要为了「实时」滥用双向流——大部分场景服务端流就够。
6. 健康检查与网关接入
一句话总结: gRPC 健康检查让负载均衡与编排平台知道实例是否可服务,API 网关负责把外部 HTTP 请求翻译成内部 gRPC 调用。
服务注册到注册中心(Consul/Kubernetes)后,健康检查决定流量是否进入该实例。gRPC 有标准健康检查协议(grpc.health.v1.Health)。网关(如 Envoy、YARP)把外部 HTTP 请求映射为内部 gRPC,让浏览器客户端沿用熟悉的 REST 风格。
// 引入 Grpc.AspNetCore.HealthChecks
builder.Services.AddGrpcHealthChecks()
.AddCheck("orders", () => HealthCheckResult.Healthy());
app.MapGrpcHealthChecksService(); // 暴露标准 /grpc.health.v1.Health
// YARP 反向代理配置示例(appsettings.json 片段)
// 把 /api/orders/** 转发到 gRPC 服务,做 HTTP/1.1 到 HTTP/2 的翻译
| 组件 | 作用 |
|---|---|
| gRPC Health Check | 标准健康协议,编排平台探活 |
| Consul/K8s 服务发现 | 注册实例、摘除故障实例 |
| YARP/Envoy 网关 | HTTP ↔ gRPC 翻译、负载均衡 |
| gRPC-Web | 让浏览器走 gRPC(需代理) |
避坑: 健康检查要反映「真实可服务性」——不只是进程活着,还要依赖(DB、下游)可用。网关做协议翻译时注意超时与重试配置,别让网关成为故障放大器。浏览器场景优先 gRPC-Web,而不是强行暴露裸 gRPC。
7. 性能与安全调优
一句话总结: gRPC 的性能优势来自 HTTP/2 多路复用与 Protobuf 二进制,安全上要启用 TLS、控制消息大小并做限流与鉴权。
性能层面,GrpcChannel 复用 HTTP/2 连接,多个调用并发共享一条连接;服务器要合理设置 MaxReceiveMessageSize、MaxSendMessageSize 与并发限制。安全层面,生产必须 TLS(AddGrpc 默认要求),配合认证拦截器与授权策略。
builder.Services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4MB
options.MaxSendMessageSize = 4 * 1024 * 1024;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.Interceptors.Add<AuthInterceptor>();
});
// 认证:把客户端证书或 token 转为 ClaimsPrincipal
public class AuthInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request, ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var token = context.RequestHeaders.GetValue("authorization")?
.Replace("Bearer ", "");
if (!_validator.IsValid(token))
throw new RpcException(new Status(StatusCode.Unauthenticated, "bad token"));
return await continuation(request, context);
}
}
| 优化项 | 建议 |
|---|---|
| 消息大小 | 按业务设置上限,防 OOM |
| 连接复用 | 用 DI 单例 GrpcChannel |
| 并发 | 服务端默认高并发,关注下游连接池 |
| TLS | 生产强制 mTLS 或 TLS |
| 鉴权 | 拦截器 + 元数据 token |
避坑: 不做限流与大小限制的 gRPC 服务容易被恶意大消息打爆内存。
GrpcChannel要在 DI 里注册为单例复用,别每次调用都新建。调试期开EnableDetailedErrors,生产必须关闭,避免内部信息泄漏。
8. 总结
| 主题 | 要点 |
|---|---|
| 契约 | .proto 是事实来源,字段编号不可改 |
| 服务实现 | 继承生成基类,异常转 RpcException |
| 拦截器 | 认证、日志、错误转换集中治理 |
| 流式 RPC | 四类模式,关注背压与取消 |
| 健康检查 | 编排平台探活,反映真实可服务性 |
| 网关 | HTTP ↔ gRPC 翻译,浏览器走 gRPC-Web |
| 安全 | TLS 强制、限流、鉴权拦截器 |
gRPC 让微服务通信从「文档约定」走向「编译期契约」:.proto 定义了服务边界,代码生成消灭手写 DTO 与序列化样板,拦截器统一横切关注点,流式通信支撑实时场景。它不适合面向浏览器的开放 API,却是服务间通信的高性能选择。把契约当产品管理、把弹性与安全放进公共层,gRPC 就能成为微服务架构里可靠、高效、可观测的通信骨架。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。