玩转gRPC和.NET 6

188 阅读6分钟

玩转gRPC和.NET 6

在.NET中创建一个gRPC服务器。

在本教程中,我想一步一步地告诉你,如何在.NET中创建一个gRPC服务器,实现*""文件中定义的接口。原点"*文件中定义的接口。

在下一张图片中,我标记了我们将在本教程中关注的项目。

先决条件

我使用的是macOs,接下来的命令是针对这个操作系统的。

  • gRPC编译器。要安装Protobuf编译器,你可以在终端执行这个命令。
brew install protobuf
  • .NET 6 SDK。这里你可以找到下载和安装.NET 6 SDK的链接。
  • Visual Studio Code或你选择的IDE
  • grpcurl。一个提供与gRPC服务互动的命令行工具
brew install grpcurl
  • grpcui:建立在gRPCurl之上,为gRPC增加了一个交互式的Web UI,类似于Postman和Swagger UI等工具。
brew install grpcui

操作步骤

在一般情况下,我们应该遵循以下步骤:

  1. 创建一个项目并配置它以生成服务器存根。
  2. 创建proto文件
  3. 在你的项目中注册proto文件并编译项目。
  4. 实现业务逻辑。
  5. Program.cs 上注册这些类。
  6. 设置Kestrel 以启用不含TLS的HTTP/2(在开发环境下)。
  7. 启动服务器并使用客户端测试(我将使用grpcurl)。

在这个例子中,我们将使用一个名为CountryGrpcServer 的.NET项目,该项目实现了搜索、创建或获取带有一些基本数据的国家列表的业务逻辑。CountryGrpcServer.proto 文件(如下所述)声明了远程程序。

1. 创建一个项目并配置它以生成服务器存根

要创建一个gRPC模板.NET项目,请打开一个终端,执行下一个命令。

dotnet new grpc -o grpc.country.server -n CountryGrpcServer

输出结果是这样的。

这里:

  • -o 参数用于定义项目的目录名称: 。grpc.country.server
  • -n 参数用于定义项目名称: 。CountryGrpcServer

2.创建Proto文件

原理文件定义了消息(数据结构)和将由服务器公开并由客户端消费的方法。下一张图片是proto文件的简化视图。

在本教程中,我已经创建了一个CountryGrpcServer.proto 文件,并将其保存在位于项目中的Protos 目录中:

vim grpc.country.server/Protos/CountryGrpcServer.proto

复制文件中的下一行:

ProtoBuf


syntax = "proto3";

/*The Proto file that has Empty message definition*/
import "google/protobuf/empty.proto";

// Defining the namespace in which the generate classes will be 
option csharp_namespace = "Sumaris.Grpc.Services";

// The service name will be used by the compiler when generate the base classes
// Here I declare five procedure
service CountryService{
	//Server streaming RPC
    rpc getAllCountries(google.protobuf.Empty)
        returns (stream Country);
	// Unitary RPC
    rpc listAllCountries(google.protobuf.Empty)
        returns ( CountryList);
    // Unitary RPC
    rpc findCountryByName( FindCountryByNameRequest )
        returns (FindCountryByNameResponse);
	// Unitary RPC
    rpc createCountry (CountryCreateRequest)
        returns (CountryCreateRespopnse);
	// Bidrectional streaming RPC
    rpc findCountriesByNames( stream FindCountryByNameRequest)
        returns (stream Country);
}


message Country{
    string name=1;
    string capitalCity=2;
    float area=3;
}

message CountryList{repeated Country country = 1;}

message FindCountryByNameRequest{string name=1;}

message FindCountryByNameResponse{Country country=1;}

message CountryCreateRequest{ Country country=1;}

message CountryCreateRespopnse{bool created=1;}

3. 在你的项目上注册Proto文件并编译项目

为了注册项目,你可以

A.编辑csproj 文件,并添加以下几行。

XML

<ItemGroup>
    <Protobuf Include="Protos\CountryGrpcServer.proto" GrpcServices="Server">
      <Link>Protos\CountryGrpcServer.proto</Link>
      <Access>Public</Access>
      <ProtoCompile>True</ProtoCompile>
      <CompileOutputs>True</CompileOutputs>
      <OutputDir>obj\Debug\net6.0\</OutputDir>
      <Generator>MSBuild:Compile</Generator>
    </Protobuf>
</ItemGroup>

B.或者,使用Visual Studio代码,按照下面的步骤进行。

i.右键单击 "连接的服务"->"打开服务库"。

ii.选择选项 "为gRPC创建一个新的API服务参考"。

iii.选择 "添加 "按钮。

iv.选择在我们项目的Protos 目录中创建的CountryGrpcServer.proto 文件,并选择类的类型为服务器。

现在,请打开终端,进入在第一步创建的项目,并编译该项目。

dotnet build

输出结果看起来是这样的。

当你构建项目时,dotnet会在""目录下创建两个类。$ProjectPath/obj/Debug/net6.0/Protos/ 目录下的两个类。这些类是。

这两个类都在 命名空间中。这是在proto文件上声明的 的值。Sumaris.Grpc.Services option csharp_namespace

CountryGrpcServerGrpc.cs 定义了部分类CountryService 这是在proto文件上声明的服务名称),它包含一个抽象的内类,叫做CountryServiceBase (这个名字是用服务的名称加上Base这个词创建的)。

这个抽象类声明了与proto文件中定义的相同的方法,并将成为我们继承的基类,以实现我们的业务类。

4.实现业务逻辑

现在,我们将创建一个拥有我们业务逻辑的类。这个类必须从抽象的基类Sumaris.Grpc.Services.CountryService.CountryServiceBase

我已经在位于Services子目录下的文件中创建了这个类BusinessCountryService ,下面是几行。

C#

using System;
using Sumaris.Grpc.Services;
using Google.Protobuf.WellKnownTypes;
using static Sumaris.Grpc.Services.CountryService;
using Grpc.Core;

namespace CountryGrpcServer.Services
{
    public class BusinessCountryService: CountryServiceBase
    {
        private static List<Country> countries;

        static BusinessCountryService()
        {
            countries = new List<Country>();
            countries.Add(new Country() { Area = 1285216.60f, CapitalCity = "Lima", Name = "Perú" });
            countries.Add(new Country() { Area = 756102.4f, CapitalCity = "Santiago de Chile", Name = "Chile" });
            countries.Add(new Country() { Area = 283561f, CapitalCity = "Quito", Name = "Ecuador" });
            countries.Add(new Country() { Area = 1141748f, CapitalCity = "Bogotá", Name = "Colombia" });
            countries.Add(new Country() { Area = 2791820f, CapitalCity = "Buenos Aires", Name = "Argentina" });
        }

        private readonly ILogger<GreeterService> _logger;
        public BusinessCountryService(ILogger<GreeterService> logger)
        {
            _logger = logger;
        }

        /** Server streamin RPC procedure
         * rpc getAllCountries(google.protobuf.Empty) returns(stream Country)
         * We use the async and await 
        */
        public override async Task getAllCountries(Empty request,IServerStreamWriter<Country> responseStream, ServerCallContext context) {
            foreach (var t in countries) { await responseStream.WriteAsync(t); }
        }

        /** Unitary RPC procedure
         *  rpc listAllCountries(google.protobuf.Empty) returns ( CountryList);
        */
        public override Task<CountryList> listAllCountries( Empty request,ServerCallContext context)
        {
            var r = new CountryList();
            r.Country.AddRange(countries);
            countries.ForEach(t => r.Country.Add(t));
            return Task.FromResult(r);
        }

        /** Unitary RPC procedure
         *  rpc findCountryByName( FindCountryByNameRequest ) returns (FindCountryByNameResponse);
         */
        public override Task<FindCountryByNameResponse> findCountryByName(FindCountryByNameRequest request,ServerCallContext context)
        {
            var r = new FindCountryByNameResponse();
            r.Country = countries.Find(c => c.Name.ToLower().Equals(request.Name.ToLower()));
            return Task.FromResult(r);
        }

        /** Unitary RPC procedure
         *  rpc createCountry (CountryCreateRequest) returns (CountryCreateRespopnse);
         */
        public override Task<CountryCreateRespopnse> createCountry(CountryCreateRequest request,ServerCallContext context)
        {
            countries.Add(request.Country);
            var r = new CountryCreateRespopnse();
            r.Created = true;
            return Task.FromResult(r);
        }

        /** Biderectional streaming RPC
         * rpc findCountriesByNames( stream FindCountryByNameRequest) returns (stream Country);
         */
        public override async Task findCountriesByNames(IAsyncStreamReader<FindCountryByNameRequest> requestStream,
            IServerStreamWriter<Country> responseStream,ServerCallContext context){
            await foreach (var t in requestStream.ReadAllAsync())
            {
                var r = countries.Find(c => c.Name.ToLower().Equals(t.Name.ToLower()));
                if (r is not null)
                {
                    await responseStream.WriteAsync(r);
                }
            }
        }
    }
}

请注意,如果服务器必须以流的方式响应,该方法必须是异步的,并且当你在输出通道中异步写入时,必须使用await字样。

如果客户端发送一个流请求,你必须使用await字样来异步读取,并且方法必须再次被标记为异步

5.注册类Program.cs

下一步是使用WebApplication实例的方法MapGrpcService ,在Web应用程序中注册我们的业务类。

请打开Programs.cs 文件并复制下一步。

C#

app.MapGrpcService<BusinessCountryService>();

6.设置Kestrel以启用无TLS的HTTP/2(在开发环境上)

Kestrel在macOS系统上不支持带TLS的HTTP/2,我们需要关闭TLS(但是你不必在生产环境中这样做,因为你应该使用TLS)。

请在Program.cs.Net上复制下面几行。

C#

builder.WebHost.ConfigureKestrel(options =>
{
  // Setup a HTTP/2 endpoint without TLS.
  options.ListenLocalhost(5000, o => o.Protocols =
      HttpProtocols.Http2);
});

毕竟,Program.cs 文件应该是这样的。

C#

using CountryGrpcServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

// Add services to the container.
builder.Services.AddGrpc();
builder.WebHost.ConfigureKestrel(options =>
{
    // Setup a HTTP/2 endpoint without TLS.
    options.ListenLocalhost(5000, o => o.Protocols =
        HttpProtocols.Http2);
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<BusinessCountryService>();
app.Run();

7.启动服务器并使用客户端测试(我将使用grpcurl)。

打开终端,进入项目,并执行下一条命令。

dotnet run

在终端中执行下一条命令(移动到项目中)。

grpcurl -d  '{"name":"Peru"}{"name":"Ecuador"}{"name":"Chile"}' \
-proto ./Protos/CountryGrpcServer.proto \
-plaintext localhost:5000 \
CountryService/findCountriesByNames

注意,我使用的是CountryGrpcServer.proto 文件的相对路径。

现在,服务器返回。

结论

我们用协议缓冲区文件在.NET中生成了服务器实现,用async/await来管理流媒体服务器或客户端。