在构建分布式系统时,服务之间的通信方式直接决定了系统的性能上限和可维护性。传统的RESTful API配合JSON虽然简单直观,但在高并发、低延迟的内部服务调用场景下,序列化开销和网络传输体积往往成为瓶颈。gRPC作为Google开源的高性能RPC框架,基于HTTP/2协议和Protobuf二进制序列化,配合C#的原生支持,可以在.NET生态中快速构建高效的跨平台通信服务。本文将从.proto契约定义开始,一步步完成服务端和客户端的搭建,并深入讲解流式通信与生产环境的进阶配置。

为什么内部服务通信更适合gRPC
gRPC的核心优势来自两个层面。第一是协议层面,gRPC构建在HTTP/2之上,支持多路复用,多个请求可以共用一条TCP连接,避免了HTTP/1.1下的队头阻塞问题,连接建立的开销被大幅摊薄。第二是序列化层面,Protobuf使用紧凑的二进制格式编码数据,相比JSON的文本格式,同样的业务数据传输体积通常只有JSON的三分之一到五分之一,编解码速度也快得多。对于一个字段较多的订单对象,JSON可能需要几毫秒的反序列化时间,而Protobuf往往在微秒级别完成。
另一个容易被忽视的优势是契约优先的开发模式。.proto文件明确定义了服务的接口、方法签名和数据结构,客户端和服务端的代码都由工具自动生成,双方在编译期就能发现接口不匹配的问题,而不是等到运行时才抛出字段缺失的异常。这一点在多团队协作的大型项目中尤其有价值,接口文档和代码永远保持一致,不存在文档过期的困扰。
当然gRPC并不是万能的。浏览器无法直接调用gRPC(需要gRPC-Web网关),对外暴露的公开API一般还是REST更合适。gRPC的主战场是内部微服务之间的高频调用、IoT设备与服务器的低带宽通信、以及需要服务端主动推送数据的实时场景。
搭建项目并定义proto契约
首先创建解决方案,包含一个服务端和一个客户端项目。gRPC服务端推荐使用ASP.NET Core托管,客户端可以是控制台、WPF或任何.NET应用。安装必要的NuGet包:
dotnet new grpc -n GrpcOrderService dotnet new console -n GrpcOrderClient # 服务端需要(模板已内置) # Grpc.AspNetCore # 客户端需要 dotnet add ./GrpcOrderClient package Google.Protobuf dotnet add ./GrpcOrderClient package Grpc.Net.Client dotnet add ./GrpcOrderClient package Grpc.Tools
接着在服务端项目的Protos文件夹下新建order.proto文件,定义订单查询服务。注意package会影响生成代码的命名空间,csharp_namespace则显式指定C#命名空间,建议两者保持一致的层级关系:
syntax = "proto3";
option csharp_namespace = "GrpcOrderService.Protos";
package order;
service OrderService {
// 一元调用:查询订单详情
rpc GetOrder (GetOrderRequest) returns (OrderReply);
// 服务端流式:批量拉取订单
rpc ListOrders (ListOrdersRequest) returns (stream OrderReply);
// 双向流式:实时上报与推送
rpc Chat (stream ReportMessage) returns (stream PushMessage);
}
message GetOrderRequest {
int32 order_id = 1;
}
message ListOrdersRequest {
int32 page_size = 1;
}
message OrderReply {
int32 order_id = 1;
string product_name = 2;
double amount = 3;
string status = 4;
}
message ReportMessage {
string content = 1;
}
message PushMessage {
string content = 1;
}
proto文件中每个字段都有编号,这个编号是二进制编码的一部分,一旦发布就不可更改,否则会导致新旧版本不兼容。新增字段时应该使用新的编号,弃用的字段保留编号但标记为reserved,这是Protobuf兼容性管理的基本纪律。
客户端项目也需要同一份proto文件。常见做法是把proto放到共享类库中供双方引用,或者用链接文件的方式添加。在csproj中配置:
<ItemGroup> <Protobuf Include="Protos\order.proto" GrpcServices="Client" /> </ItemGroup>
GrpcServices属性控制生成的内容,服务端填Server会生成基类,客户端填Client会生成调用桩,两者都填Both则生成完整代码。编译项目后,OrderService.OrderServiceClient等类型就可以直接使用了。
实现服务端与客户端调用
服务端实现需要继承生成的基类,重写每个rpc方法。在Startup或Program中(.NET 6之后的 Minimal API 风格)注册gRPC服务即可。下面是完整的服务端实现,包含一元调用和服务端流式调用:
using Grpc.Core;
using GrpcOrderService.Protos;
public class OrderServiceImpl : OrderService.OrderServiceBase
{
private readonly ILogger<OrderServiceImpl> _logger;
public OrderServiceImpl(ILogger<OrderServiceImpl> logger)
{
_logger = logger;
}
// 一元调用
public override Task<OrderReply> GetOrder(GetOrderRequest request, ServerCallContext context)
{
_logger.LogInformation("收到查询请求,订单号: {OrderId}", request.OrderId);
return Task.FromResult(new OrderReply
{
OrderId = request.OrderId,
ProductName = "机械键盘",
Amount = 499.0,
Status = "已发货"
});
}
// 服务端流式调用
public override async Task ListOrders(ListOrdersRequest request,
IServerStreamWriter<OrderReply> responseStream, ServerCallContext context)
{
for (int i = 1; i <= request.PageSize; i++)
{
// 客户端取消时及时退出
if (context.CancellationToken.IsCancellationRequested)
{
break;
}
await responseStream.WriteAsync(new OrderReply
{
OrderId = i,
ProductName = $"商品{i}",
Amount = i * 10.5,
Status = "已支付"
});
await Task.Delay(200); // 模拟数据生产耗时
}
}
}
服务端注册部分非常简洁,在Program.cs中一行代码即可完成:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc(); // 可在此配置重试、拦截器等 var app = builder.Build(); app.MapGrpcService<OrderServiceImpl>(); app.Run();
客户端的调用同样直观。一元调用是同步阻塞式的请求响应模型,流式调用则通过AsyncServerStreamingCall持续读取服务端推送的数据:
using Grpc.Net.Client;
using GrpcOrderClient.Protos;
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new OrderService.OrderServiceClient(channel);
// 一元调用
var order = await client.GetOrderAsync(new GetOrderRequest { OrderId = 1001 });
Console.WriteLine($"订单: {order.ProductName}, 金额: {order.Amount}");
// 服务端流式调用
using var call = client.ListOrders(new ListOrdersRequest { PageSize = 10 });
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"收到流式数据: {reply.OrderId} - {reply.ProductName}");
}
值得强调的是channel的生命周期管理。GrpcChannel内部维护连接池和HTTP/2多路复用,属于重量级对象,应该在整个应用生命周期内复用一个实例,而不是每次调用都创建。频繁创建通道不仅浪费资源,还会导致连接风暴。
生产环境的进阶配置
真实项目中,超时和错误处理是绕不开的话题。gRPC提供了deadline机制,超过指定时间后调用自动取消,服务端也能感知到取消信号并停止后续计算:
try
{
var reply = await client.GetOrderAsync(
new GetOrderRequest { OrderId = 1001 },
deadline: DateTime.UtcNow.AddSeconds(3));
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.DeadlineExceeded)
{
Console.WriteLine("调用超时");
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.Unavailable)
{
Console.WriteLine("服务不可用,可触发重试逻辑");
}
对于瞬时故障,可以启用内置的透明重试。在服务端配置重试策略后,客户端会在遇到可重试状态码时自动重发请求,业务代码完全无感知:
// 服务端 Program.cs
builder.Services.AddGrpc(o =>
{
o.EnableRetry = true;
});
// 客户端配置重试策略
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
ServiceConfig = new ServiceConfig
{
MethodConfigs =
{
new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
RetryableStatusCodes = { StatusCode.Unavailable }
}
}
}
}
});
另外两点实践建议:一是健康检查,gRPC有一套标准的健康检查协议grpc.health.v1,配合Kubernetes的liveness探针可以让编排系统准确感知服务状态;二是负载均衡,客户端侧可以配置静态负载均衡策略,或在服务网格中由Sidecar统一处理。日志与监控方面,建议注册拦截器统一记录调用耗时和状态码,避免在每个方法里重复写埋点代码。
gRPC与REST的选型建议
最后回到架构层面做一次取舍分析。如果接口是面向浏览器或第三方开发者的公开API,REST加JSON仍是首选,生态成熟、调试方便、任意HTTP客户端都能调用。而内部微服务之间、对延迟敏感的调用链路、需要双向流的实时场景(如即时通讯、行情推送、日志采集),gRPC的优势非常明显。两者并非互斥,一个健康的系统完全可以对外REST、对内gRPC,通过网关层做协议转换,各取所长。掌握了proto契约定义、代码生成、流式调用和超时重试这些核心技能后,你就可以在C#项目中自信地落地gRPC了。