名称:dotnet-grpc 描述:“构建gRPC服务。Proto定义、代码生成、ASP.NET Core主机、流式传输、认证。” 用户可调用:false
dotnet-grpc
用于.NET应用程序的完整gRPC生命周期。涵盖.proto服务定义、代码生成、ASP.NET Core gRPC服务器实现和端点托管、Grpc.Net.Client客户端模式、所有四种流模式(一元、服务器流、客户端流、双向流)、认证、负载均衡和健康检查。
范围
- Proto服务定义和代码生成
- ASP.NET Core gRPC服务器实现
- Grpc.Net.Client客户端模式
- 所有四种流模式(一元、服务器流、客户端流、双向流)
- 认证、负载均衡和健康检查
范围外
- 源生成器编写模式 – 参见[技能:dotnet-csharp-source-generators]
- HTTP客户端工厂和弹性管道 – 参见[技能:dotnet-http-client]和[技能:dotnet-resilience]
- 原生AOT架构和修剪 – 参见[技能:dotnet-native-aot]和[技能:dotnet-trimming]
交叉引用:[技能:dotnet-resilience]用于gRPC通道的重试/断路器,[技能:dotnet-serialization]用于Protobuf线格式详情。参见[技能:dotnet-integration-testing]用于测试gRPC服务。
Proto定义和代码生成
项目设置
gRPC使用Protocol Buffers作为其接口定义语言。Grpc.Tools包在构建时从.proto文件生成C#代码。
服务器项目:
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.*" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
</ItemGroup>
客户端项目:
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Net.Client" Version="2.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
</ItemGroup>
共享合约项目(推荐用于大型服务):
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
Proto文件定义
syntax = "proto3";
option csharp_namespace = "MyApp.Grpc";
package myapp;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// 服务定义,包含所有4种流模式
service OrderService {
// 一元:单个请求,单个响应
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
// 服务器流:单个请求,响应流
rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
// 客户端流:请求流,单个响应
rpc UploadOrders (stream CreateOrderRequest) returns (UploadOrdersResponse);
// 双向流:请求流,响应流
rpc ProcessOrders (stream CreateOrderRequest) returns (stream OrderResponse);
}
message GetOrderRequest {
int32 id = 1;
}
message ListOrdersRequest {
string customer_id = 1;
int32 page_size = 2;
string page_token = 3;
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItemMessage items = 2;
}
message OrderResponse {
int32 id = 1;
string customer_id = 2;
repeated OrderItemMessage items = 3;
google.protobuf.Timestamp created_at = 4;
}
message OrderItemMessage {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
message UploadOrdersResponse {
int32 orders_created = 1;
}
代码生成工作流
Grpc.Tools包在构建时运行Protobuf编译器(protoc)和C# gRPC插件。生成的文件出现在obj/中并自动包含:
- 通过
<Protobuf>项将.proto文件添加到项目 - 设置
GrpcServices为Server、Client或Both - 构建项目 – 生成的C#类型和服务存根出现在
obj/Debug/net10.0/Protos/ - 实现生成的抽象基类(服务器)或使用生成的客户端类
gRPC代码生成工具链使用源生成从.proto定义生成C#存根。这概念上类似于[技能:dotnet-csharp-source-generators],但使用protoc而非Roslyn增量生成器。
ASP.NET Core gRPC服务器
服务实现
实现生成的抽象基类:
using Grpc.Core;
using MyApp.Grpc;
public sealed class OrderGrpcService(
OrderRepository repository,
ILogger<OrderGrpcService> logger) : OrderService.OrderServiceBase
{
// 一元
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
var order = await repository.GetByIdAsync(request.Id, context.CancellationToken);
if (order is null)
{
throw new RpcException(new Status(StatusCode.NotFound,
$"订单 {request.Id} 未找到"));
}
return MapToResponse(order);
}
// 服务器流
public override async Task ListOrders(
ListOrdersRequest request,
IServerStreamWriter<OrderResponse> responseStream,
ServerCallContext context)
{
await foreach (var order in repository.ListByCustomerAsync(
request.CustomerId, context.CancellationToken))
{
await responseStream.WriteAsync(MapToResponse(order),
context.CancellationToken);
}
}
// 客户端流
public override async Task<UploadOrdersResponse> UploadOrders(
IAsyncStreamReader<CreateOrderRequest> requestStream,
ServerCallContext context)
{
var count = 0;
await foreach (var request in requestStream.ReadAllAsync(
context.CancellationToken))
{
await repository.CreateAsync(MapFromRequest(request),
context.CancellationToken);
count++;
}
return new UploadOrdersResponse { OrdersCreated = count };
}
// 双向流
public override async Task ProcessOrders(
IAsyncStreamReader<CreateOrderRequest> requestStream,
IServerStreamWriter<OrderResponse> responseStream,
ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync(
context.CancellationToken))
{
var order = await repository.CreateAsync(MapFromRequest(request),
context.CancellationToken);
await responseStream.WriteAsync(MapToResponse(order),
context.CancellationToken);
}
}
private static OrderResponse MapToResponse(Order order) =>
new()
{
Id = order.Id,
CustomerId = order.CustomerId,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(
order.CreatedAt)
};
private static Order MapFromRequest(CreateOrderRequest request) =>
new()
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = (decimal)i.UnitPrice
}).ToList()
};
}
端点托管
在ASP.NET Core管道中注册gRPC服务:
var builder = WebApplication.CreateBuilder(args);
// 添加gRPC服务
builder.Services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
options.MaxSendMessageSize = 4 * 1024 * 1024;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
var app = builder.Build();
// 映射gRPC服务端点
app.MapGrpcService<OrderGrpcService>();
app.Run();
gRPC反射(开发)
启用gRPC反射以用于工具如grpcurl和grpcui:
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
var app = builder.Build();
app.MapGrpcService<OrderGrpcService>();
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}
使用Grpc.Net.Client的客户端模式
基本客户端
using Grpc.Net.Client;
using MyApp.Grpc;
// 创建通道(跨调用重用 -- 通道创建成本高)
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new OrderService.OrderServiceClient(channel);
// 一元调用
var response = await client.GetOrderAsync(
new GetOrderRequest { Id = 42 });
通过IHttpClientFactory注册的DI客户端
通过IHttpClientFactory注册gRPC客户端以进行连接池和弹性:
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.ConfigureChannel(options =>
{
options.MaxReceiveMessageSize = 4 * 1024 * 1024;
});
通过[技能:dotnet-resilience]应用弹性:
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.AddStandardResilienceHandler();
读取服务器流
using var call = client.ListOrders(
new ListOrdersRequest { CustomerId = "cust-123" });
await foreach (var order in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"订单 {order.Id}: {order.CustomerId}");
}
客户端流
using var call = client.UploadOrders();
foreach (var order in ordersToCreate)
{
await call.RequestStream.WriteAsync(new CreateOrderRequest
{
CustomerId = order.CustomerId
});
}
// 发送完成信号
await call.RequestStream.CompleteAsync();
// 读取响应
var response = await call;
Console.WriteLine($"创建了 {response.OrdersCreated} 个订单");
双向流
using var call = client.ProcessOrders();
// 在后台开始读取响应
var readTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"处理了订单 {response.Id}");
}
});
// 发送请求
foreach (var order in ordersToProcess)
{
await call.RequestStream.WriteAsync(new CreateOrderRequest
{
CustomerId = order.CustomerId
});
}
await call.RequestStream.CompleteAsync();
await readTask;
流模式总结
gRPC支持四种通信模式:
| 模式 | 请求 | 响应 | 用例 |
|---|---|---|---|
| 一元 | 单个消息 | 单个消息 | 标准请求-响应(CRUD、查询) |
| 服务器流 | 单个消息 | 消息流 | 实时馈送、大型结果集、推送通知 |
| 客户端流 | 消息流 | 单个消息 | 批量上传、聚合、遥测摄取 |
| 双向流 | 消息流 | 消息流 | 聊天、实时协作、事件处理 |
认证
Bearer令牌(JWT)
服务器端认证:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://identity.example.com";
options.TokenValidationParameters.ValidAudience = "order-api";
});
builder.Services.AddAuthorization();
builder.Services.AddGrpc();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGrpcService<OrderGrpcService>().RequireAuthorization();
客户端令牌传播:
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.AddCallCredentials(async (context, metadata, serviceProvider) =>
{
var tokenProvider = serviceProvider.GetRequiredService<ITokenProvider>();
var token = await tokenProvider.GetTokenAsync(context.CancellationToken);
metadata.Add("Authorization", $"Bearer {token}");
});
证书认证(mTLS)
用于服务到服务认证的相互TLS:
// 服务器:要求客户端证书
builder.WebHost.ConfigureKestrel(kestrel =>
{
kestrel.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.NoCheck; // 按环境配置
});
// 客户端:提供客户端证书
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(
new X509Certificate2("client.pfx", "password"));
using var channel = GrpcChannel.ForAddress("https://order-service:5001",
new GrpcChannelOptions
{
HttpHandler = handler
});
负载均衡
客户端负载均衡
gRPC支持带服务发现的客户端负载均衡:
// 基于DNS的服务发现,轮询
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("dns:///order-service:5001");
})
.ConfigureChannel(options =>
{
options.Credentials = ChannelCredentials.Insecure;
options.ServiceConfig = new ServiceConfig
{
LoadBalancingConfigs = { new RoundRobinConfig() }
};
});
基于代理的负载均衡
对于有负载均衡器的环境(如Kubernetes、Envoy、YARP):
- 使用L7(HTTP/2感知)负载均衡器 – L4负载均衡器在TCP级别路由,并将所有gRPC请求固定到单个后端,因为HTTP/2在单个连接上复用。
- Envoy、Linkerd和支持gRPC的Kubernetes ingress控制器在RPC级别分发请求。
- 配置
SocketsHttpHandler.EnableMultipleHttp2Connections = true以允许在代理后多个连接:
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service-lb:5001");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true
});
健康检查
gRPC健康检查协议
实现标准gRPC健康检查协议(grpc.health.v1.Health),以便编排器和负载均衡器可以探测服务状态:
builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks()
.AddCheck("database", () =>
{
// 自定义健康检查逻辑
return HealthCheckResult.Healthy();
});
var app = builder.Build();
app.MapGrpcService<OrderGrpcService>();
app.MapGrpcHealthChecksService();
与ASP.NET Core健康检查集成
gRPC健康检查与标准ASP.NET Core健康检查系统集成:
builder.Services.AddHealthChecks()
.AddNpgSql(
builder.Configuration.GetConnectionString("OrderDb")!,
name: "order-db",
tags: ["ready"]);
builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks()
.AddAsyncCheck("order-db", async (sp, ct) =>
{
var healthCheckService = sp.GetRequiredService<HealthCheckService>();
var report = await healthCheckService.CheckHealthAsync(
r => r.Tags.Contains("ready"), ct);
return report.Status == HealthStatus.Healthy
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy();
});
Kubernetes的gRPC探针
# 使用gRPC健康检查探针(Kubernetes 1.24+)
livenessProbe:
grpc:
port: 5001
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
grpc:
port: 5001
initialDelaySeconds: 5
periodSeconds: 10
拦截器
gRPC拦截器是用于gRPC调用的中间件,类似于ASP.NET Core中间件或HTTP DelegatingHandlers。
服务器拦截器
public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger)
: Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var stopwatch = Stopwatch.StartNew();
try
{
var response = await continuation(request, context);
logger.LogInformation(
"gRPC {Method} 在 {ElapsedMs}ms 内完成",
context.Method, stopwatch.ElapsedMilliseconds);
return response;
}
catch (RpcException ex)
{
logger.LogError(ex,
"gRPC {Method} 失败,状态码 {StatusCode}",
context.Method, ex.StatusCode);
throw;
}
}
}
// 注册
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
客户端拦截器
public sealed class AuthInterceptor(ITokenProvider tokenProvider) : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var token = tokenProvider.GetCachedToken();
var headers = context.Options.Headers ?? new Metadata();
headers.Add("Authorization", $"Bearer {token}");
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method, context.Host,
context.Options.WithHeaders(headers));
return continuation(request, newContext);
}
}
错误处理
状态码
将域错误映射到gRPC状态码:
| gRPC 状态 | HTTP 等价 | 使用时机 |
|---|---|---|
OK |
200 | 成功 |
NotFound |
404 | 资源不存在 |
InvalidArgument |
400 | 客户端发送了错误数据 |
PermissionDenied |
403 | 调用者缺乏权限 |
Unauthenticated |
401 | 无有效凭据 |
AlreadyExists |
409 | 重复创建尝试 |
ResourceExhausted |
429 | 速率限制 |
Internal |
500 | 未处理的服务器错误 |
Unavailable |
503 | 瞬态失败 – 可以安全重试 |
DeadlineExceeded |
504 | 操作超时 |
丰富错误详情
// 服务器:抛出带元数据
var status = new Status(StatusCode.InvalidArgument, "验证失败");
var metadata = new Metadata
{
{ "field", "customer_id" },
{ "reason", "客户ID是必需的" }
};
throw new RpcException(status, metadata);
// 客户端:读取错误元数据
try
{
var response = await client.GetOrderAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
var field = ex.Trailers.GetValue("field");
var reason = ex.Trailers.GetValue("reason");
logger.LogWarning("验证错误在 {Field}: {Reason}", field, reason);
}
截止时间和取消
始终在gRPC调用上设置截止时间,以防止无限等待:
// 客户端:设置截止时间
var deadline = DateTime.UtcNow.AddSeconds(10);
var response = await client.GetOrderAsync(
new GetOrderRequest { Id = 42 },
deadline: deadline);
// 服务器:检查截止时间并传播取消
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
// context.CancellationToken 在截止时间到期时自动取消
var order = await repository.GetByIdAsync(request.Id, context.CancellationToken);
// ...
}
用于浏览器客户端的gRPC-Web
浏览器不支持原生gRPC所需的HTTP/2尾部。gRPC-Web是一种协议变体,可以在没有尾部的情况下通过HTTP/1.1和HTTP/2工作,使浏览器JavaScript客户端能够调用gRPC服务。
服务器配置
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
options.AddPolicy("GrpcWeb", policy =>
{
policy.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding");
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors();
app.UseGrpcWeb(); // 必须在UseRouting和MapGrpcService之间
app.MapGrpcService<OrderGrpcService>()
.EnableGrpcWeb()
.RequireCors("GrpcWeb");
JavaScript客户端(grpc-web)
// 使用 @improbable-eng/grpc-web 或 grpc-web 包
import { OrderServiceClient } from './generated/order_grpc_web_pb';
import { GetOrderRequest } from './generated/order_pb';
const client = new OrderServiceClient('https://api.example.com');
const request = new GetOrderRequest();
request.setId(42);
client.getOrder(request, {}, (err, response) => {
if (err) {
console.error('gRPC错误:', err.message);
return;
}
console.log('订单:', response.toObject());
});
Envoy代理替代方案
除了ASP.NET Core gRPC-Web中间件,可以使用Envoy代理将gRPC-Web请求转换为原生gRPC。这在gRPC服务无法修改时很有用:
# Envoy过滤器配置
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
gRPC-Web限制
- 仅支持一元和服务器流 – 客户端流和双向流不被gRPC-Web支持
- 无HTTP/2尾部 – 状态和尾部元数据在响应体中编码
- 需要CORS – 跨源请求需要在服务器上明确配置CORS
- 考虑SignalR用于全双工浏览器通信 – 参见[技能:dotnet-realtime-communication]用于需要双向流时的替代方案
关键原则
- 使用
.proto文件作为合约 – 它们是API形状的单一事实来源,在客户端和服务器之间共享 - 在
<Protobuf>项上设置GrpcServices–Server用于服务项目,Client用于消费者项目,Both用于共享合约 - 重用通道 –
GrpcChannel管理HTTP/2连接;为每个调用创建新通道浪费资源 - 通过DI注册gRPC客户端 –
AddGrpcClient与IHttpClientFactory集成以进行连接池和弹性 - 始终设置截止时间 – 没有截止时间的调用可能会无限挂起,如果服务器慢或不可达
- 使用L7负载均衡器 – L4负载均衡器将所有流量固定到一个后端,因为HTTP/2在单个TCP连接上复用
- 实现gRPC健康检查协议 – 使Kubernetes探针和负载均衡器能够监控服务健康
- 对浏览器客户端使用gRPC-Web – 原生gRPC需要浏览器不支持的HTTP/2尾部;gRPC-Web桥接此差距
参见[技能:dotnet-native-aot]用于原生AOT编译流水线和[技能:dotnet-aot-architecture]用于构建gRPC服务时的AOT兼容模式。
代理注意事项
- 不要为每个请求创建新的
GrpcChannel– 通道创建成本高并管理HTTP/2连接。重用它们或使用DI注册的客户端。 - 不要在
<Protobuf>项上省略GrpcServices– 默认为Both,这会生成服务器和客户端存根。这会膨胀客户端项目未使用的服务器代码,反之亦然。 - 不要在不启用
EnableMultipleHttp2Connections的情况下对gRPC使用L4负载均衡器 – HTTP/2复用意味着单个连接处理所有RPC,破坏了负载分布。 - 不要从gRPC服务抛出通用
Exception– 抛出带有适当StatusCode和描述性消息的RpcException。未处理的异常变为StatusCode.Internal且无有用详情。 - 不要忘记在客户端流上调用
CompleteAsync()– 服务器在发送响应之前等待流完成。忘记这会导致调用挂起。 - 不要在未注册健康检查的情况下使用
grpc.health.v1.Health– 空健康服务总是报告Serving,这破坏了健康监控的目的。 - 不要在不配置CORS的情况下全局启用gRPC-Web –
UseGrpcWeb()没有CORS策略允许任何来源调用您的gRPC服务。始终与明确RequireCors()配对。 - 不要尝试使用gRPC-Web进行客户端流或双向流 – gRPC-Web协议仅支持一元和服务器流。使用SignalR或原生gRPC用于全双工浏览器通信。
归属
改编自Aaronontheweb/dotnet-skills(MIT许可证)。