通过ASP.NET Core开发gRPC服务实现高性能远程过程调用插图

通过ASP.NET Core开发gRPC服务实现高性能远程过程调用

你好,我是源码库的技术博主。在微服务架构大行其道的今天,服务间的通信效率直接决定了系统的整体性能。你是否曾为REST API的JSON序列化开销和HTTP/1.1的队头阻塞感到头疼?今天,我想和你分享一个我最近在项目中深度实践的技术方案:使用ASP.NET Core构建gRPC服务。它基于HTTP/2和ProtoBuf,能带来显著的性能提升和强类型接口体验。下面,我将带你从零开始,一步步搭建一个完整的gRPC服务与客户端,并分享一些我踩过的“坑”和最佳实践。

一、 项目准备与环境搭建

首先,我们需要一个清晰的目标。我们将创建一个简单的“订单查询”服务。服务端用ASP.NET Core实现,客户端是一个控制台应用。确保你的开发环境满足以下条件:

  • .NET 8 SDK 或更高版本(本文基于.NET 8)。
  • Visual Studio 2022、Rider或VS Code。

打开你的终端或IDE,我们开始创建解决方案和项目。我习惯先建一个解决方案目录,让结构更清晰。

mkdir GrpcOrderDemo
cd GrpcOrderDemo
dotnet new sln -n GrpcOrderDemo

接下来,创建服务端项目。gRPC模板已经集成在ASP.NET Core Web API模板中。

dotnet new webapi -n OrderService.Grpc --use-controllers false
dotnet sln add OrderService.Grpc

然后,创建客户端项目。我们用一个控制台程序来模拟消费者。

dotnet new console -n OrderClient
dotnet sln add OrderClient

现在,为两个项目添加必要的NuGet包。这是关键一步,版本一致性很重要,我建议在项目文件中统一指定版本。

OrderService.Grpc.csproj 中,确保包含:


OrderClient.csproj 中,添加:




踩坑提示Grpc.Tools 包必须设置 PrivateAssets="All",这样它只在编译时用于生成代码,不会作为依赖传递到输出程序集中。

二、 定义服务契约:编写.proto文件

gRPC的核心是协议缓冲区(Protocol Buffers)语言定义的服务契约。这就像一份双方都必须遵守的强类型API合同。在解决方案根目录下创建一个 Protos 文件夹,然后新建 order.proto 文件。

syntax = "proto3";

option csharp_namespace = "GrpcOrderDemo";

package order;

// 定义服务
service OrderService {
  rpc GetOrder (GetOrderRequest) returns (OrderReply);
  rpc GetOrderStream (GetOrderRequest) returns (stream OrderItem); // 服务端流示例
}

// 请求消息
message GetOrderRequest {
  string order_id = 1;
}

// 响应消息
message OrderReply {
  string order_id = 1;
  string customer_name = 2;
  double total_amount = 3;
  repeated OrderItem items = 4; // repeated 表示列表
  OrderStatus status = 5;
}

// 子消息
message OrderItem {
  string product_id = 1;
  string product_name = 2;
  int32 quantity = 3;
  double unit_price = 4;
}

// 枚举
enum OrderStatus {
  UNKNOWN = 0;
  CREATED = 1;
  PAID = 2;
  SHIPPED = 3;
  DELIVERED = 4;
}

这个文件定义了一个包含两个方法的服务:一个普通RPC调用,一个服务端流式调用。注意字段后面的数字是字段编号,一旦定义,在生产环境中就绝不能修改,这是ProtoBuf的兼容性规则。

接下来,我们需要让两个项目都能使用这个契约。编辑项目文件,将.proto文件包含进来。

OrderService.Grpc.csproj 中添加:


  

OrderClient.csproj 中添加:


  

GrpcServices 属性告诉工具生成服务器端桩代码、客户端桩代码,或者两者都生成(Both)。现在编译一下项目,你会在 obj/Debug/net8.0 目录下看到生成的C#文件。这些文件包含了强类型的请求、响应类以及服务基类和客户端存根。

三、 实现gRPC服务端

服务端实现很简单,继承自生成的服务基类并重写方法。在服务端项目 OrderService.Grpc 中,创建 Services 文件夹,并添加 OrderServiceImpl.cs

using Grpc.Core;
using GrpcOrderDemo;

namespace OrderService.Grpc.Services;

public class OrderServiceImpl : OrderService.OrderServiceBase
{
    private readonly ILogger _logger;
    // 模拟一个内存数据源
    private static readonly Dictionary _orderData = new()
    {
        ["ORD-001"] = new OrderReply
        {
            OrderId = "ORD-001",
            CustomerName = "张三",
            TotalAmount = 299.98,
            Status = OrderStatus.Paid,
            Items =
            {
                new OrderItem { ProductId = "P-100", ProductName = "《ASP.NET Core实战》", Quantity = 1, UnitPrice = 89.99 },
                new OrderItem { ProductId = "P-101", ProductName = "《gRPC指南》", Quantity = 1, UnitPrice = 109.99 }
            }
        }
    };

    public OrderServiceImpl(ILogger logger)
    {
        _logger = logger;
    }

    public override Task GetOrder(GetOrderRequest request, ServerCallContext context)
    {
        _logger.LogInformation("收到订单查询请求,ID: {OrderId}", request.OrderId);

        if (_orderData.TryGetValue(request.OrderId, out var order))
        {
            return Task.FromResult(order);
        }

        // gRPC错误处理:抛出RpcException
        throw new RpcException(new Status(StatusCode.NotFound, $"订单 {request.OrderId} 不存在"));
    }

    public override async Task GetOrderStream(GetOrderRequest request, IServerStreamWriter responseStream, ServerCallContext context)
    {
        _logger.LogInformation("开始流式返回订单 {OrderId} 的商品项", request.OrderId);

        if (!_orderData.TryGetValue(request.OrderId, out var order))
        {
            throw new RpcException(new Status(StatusCode.NotFound, $"订单 {request.OrderId} 不存在"));
        }

        foreach (var item in order.Items)
        {
            // 模拟一些处理延迟
            await Task.Delay(500);
            // 检查客户端是否已取消请求(例如关闭了连接)
            if (context.CancellationToken.IsCancellationRequested)
            {
                _logger.LogInformation("客户端取消了流式请求。");
                break;
            }
            await responseStream.WriteAsync(item);
            _logger.LogInformation("已流式发送商品: {ProductName}", item.ProductName);
        }
        _logger.LogInformation("订单商品流式发送完毕。");
    }
}

接下来,在 Program.cs 中注册gRPC服务并映射端点。这是ASP.NET Core的常规操作。

using OrderService.Grpc.Services;

var builder = WebApplication.CreateBuilder(args);

// 添加gRPC服务
builder.Services.AddGrpc();

var app = builder.Build();

// 配置HTTP请求管道
app.MapGrpcService();
// 添加一个健康检查端点,便于容器编排
app.MapGet("/", () => "gRPC服务运行中。通信需要通过gRPC客户端。");

app.Run();

最后,修改 appsettings.json 中的 Kestrel 配置,确保它支持HTTP/2(gRPC的传输基础)。gRPC over HTTP/2是默认支持的,但明确配置是个好习惯。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  },
  "AllowedHosts": "*"
}

现在,运行服务端项目,它将在 https://localhost:5001http://localhost:5000 上监听。注意控制台输出,gRPC服务现在已准备就绪。

四、 实现gRPC客户端

客户端实现同样直观。我们使用生成的强类型客户端。打开 OrderClient 项目的 Program.cs

using Grpc.Core;
using Grpc.Net.Client;
using GrpcOrderDemo;

// 创建gRPC通道。通道是长期存活的,应该复用。
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
// 创建客户端实例
var client = new OrderService.OrderServiceClient(channel);

try
{
    Console.WriteLine("=== 开始普通RPC调用 ===");
    var request = new GetOrderRequest { OrderId = "ORD-001" };
    var reply = await client.GetOrderAsync(request);
    Console.WriteLine($"收到订单回复:");
    Console.WriteLine($"  订单号: {reply.OrderId}");
    Console.WriteLine($"  客户: {reply.CustomerName}");
    Console.WriteLine($"  总金额: {reply.TotalAmount:C}");
    Console.WriteLine($"  状态: {reply.Status}");

    Console.WriteLine("n=== 开始服务端流式调用 ===");
    var streamCall = client.GetOrderStream(request);
    // 异步遍历响应流
    await foreach (var item in streamCall.ResponseStream.ReadAllAsync())
    {
        Console.WriteLine($"  商品: {item.ProductName}, 数量: {item.Quantity}, 单价: {item.UnitPrice:C}");
    }
    Console.WriteLine("流式调用结束。");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine($"错误: {ex.Status.Detail}");
}
catch (RpcException ex)
{
    Console.WriteLine($"gRPC错误: {ex.Status.Code} - {ex.Status.Detail}");
}

实战经验GrpcChannel 的创建开销较大,在真实应用中(如ASP.NET Core客户端),你应该通过依赖注入将其注册为单例(AddSingleton)并进行复用。另外,注意处理 RpcException,这是gRPC标准的错误传递方式。

五、 测试、部署与进阶思考

现在,先运行服务端,再运行客户端。你应该能在客户端控制台看到订单的详细信息,以及商品项一条条“流”出来的效果。这演示了gRPC流式处理的能力,非常适合大数据集或实时通知场景。

关于部署,在Docker或Kubernetes中部署gRPC服务与部署普通ASP.NET Core应用无异。但需要注意:

  1. 负载均衡:gRPC基于HTTP/2长连接,传统的L4负载均衡可能导致连接不均衡。建议使用L7负载均衡器(如Envoy, Linkerd, 或支持HTTP/2的Nginx)或客户端负载均衡。
  2. 健康检查:使用 Grpc.AspNetCore.HealthChecks 包提供标准的gRPC健康检查协议。
  3. 安全性:生产环境务必使用TLS。在Kestrel配置中启用HTTPS并强制使用HTTP/2。

最后,gRPC并非银弹。它的强类型和性能优势在内部微服务间通信中非常突出,但对于需要暴露给外部浏览器或移动端的情况,gRPC-Web是一个补充方案。同时,像服务网格(Service Mesh)这样的基础设施正在让gRPC这类高性能RPC变得更加易用和强大。

希望这篇教程能帮你顺利开启ASP.NET Core gRPC之旅。如果在实践中遇到问题,欢迎在源码库社区交流讨论。记住,理解底层协议(HTTP/2, ProtoBuf)和工具链的运作方式,是解决复杂问题的关键。祝你编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。