.NETgRPC开发 dotnet-grpc

这个技能专注于在.NET框架中使用gRPC进行微服务开发。它涵盖了Proto服务定义、代码生成、ASP.NET Core gRPC服务器实现、客户端模式、四种流模式(一元、服务器流、客户端流、双向流)、认证、负载均衡和健康检查。关键词:gRPC, .NET, 微服务, 后端开发, Protocol Buffers, ASP.NET Core, 代码生成, 流式传输, 负载均衡, 认证。

后端开发 0 次安装 0 次浏览 更新于 3/6/2026

名称: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/中并自动包含:

  1. 通过<Protobuf>项将.proto文件添加到项目
  2. 设置GrpcServicesServerClientBoth
  3. 构建项目 – 生成的C#类型和服务存根出现在obj/Debug/net10.0/Protos/
  4. 实现生成的抽象基类(服务器)或使用生成的客户端类

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反射以用于工具如grpcurlgrpcui

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>项上设置GrpcServicesServer用于服务项目,Client用于消费者项目,Both用于共享合约
  • 重用通道GrpcChannel管理HTTP/2连接;为每个调用创建新通道浪费资源
  • 通过DI注册gRPC客户端AddGrpcClientIHttpClientFactory集成以进行连接池和弹性
  • 始终设置截止时间 – 没有截止时间的调用可能会无限挂起,如果服务器慢或不可达
  • 使用L7负载均衡器 – L4负载均衡器将所有流量固定到一个后端,因为HTTP/2在单个TCP连接上复用
  • 实现gRPC健康检查协议 – 使Kubernetes探针和负载均衡器能够监控服务健康
  • 对浏览器客户端使用gRPC-Web – 原生gRPC需要浏览器不支持的HTTP/2尾部;gRPC-Web桥接此差距

参见[技能:dotnet-native-aot]用于原生AOT编译流水线和[技能:dotnet-aot-architecture]用于构建gRPC服务时的AOT兼容模式。


代理注意事项

  1. 不要为每个请求创建新的GrpcChannel – 通道创建成本高并管理HTTP/2连接。重用它们或使用DI注册的客户端。
  2. 不要在<Protobuf>项上省略GrpcServices – 默认为Both,这会生成服务器和客户端存根。这会膨胀客户端项目未使用的服务器代码,反之亦然。
  3. 不要在不启用EnableMultipleHttp2Connections的情况下对gRPC使用L4负载均衡器 – HTTP/2复用意味着单个连接处理所有RPC,破坏了负载分布。
  4. 不要从gRPC服务抛出通用Exception – 抛出带有适当StatusCode和描述性消息的RpcException。未处理的异常变为StatusCode.Internal且无有用详情。
  5. 不要忘记在客户端流上调用CompleteAsync() – 服务器在发送响应之前等待流完成。忘记这会导致调用挂起。
  6. 不要在未注册健康检查的情况下使用grpc.health.v1.Health – 空健康服务总是报告Serving,这破坏了健康监控的目的。
  7. 不要在不配置CORS的情况下全局启用gRPC-WebUseGrpcWeb()没有CORS策略允许任何来源调用您的gRPC服务。始终与明确RequireCors()配对。
  8. 不要尝试使用gRPC-Web进行客户端流或双向流 – gRPC-Web协议仅支持一元和服务器流。使用SignalR或原生gRPC用于全双工浏览器通信。

归属

改编自Aaronontheweb/dotnet-skills(MIT许可证)。


参考资料