玩转gRPC和.NET 6
在.NET中创建一个gRPC服务器。
在本教程中,我想一步一步地告诉你,如何在.NET中创建一个gRPC服务器,实现*""文件中定义的接口。原点"*文件中定义的接口。
在下一张图片中,我标记了我们将在本教程中关注的项目。
先决条件
我使用的是macOs,接下来的命令是针对这个操作系统的。
- gRPC编译器。要安装Protobuf编译器,你可以在终端执行这个命令。
brew install protobuf
brew install grpcurl
- grpcui:建立在gRPCurl之上,为gRPC增加了一个交互式的Web UI,类似于Postman和Swagger UI等工具。
brew install grpcui
操作步骤
在一般情况下,我们应该遵循以下步骤:
- 创建一个项目并配置它以生成服务器存根。
- 创建proto文件。
- 在你的项目中注册proto文件并编译项目。
- 实现业务逻辑。
- 在
Program.cs上注册这些类。 - 设置Kestrel 以启用不含TLS的HTTP/2(在开发环境下)。
- 启动服务器并使用客户端测试(我将使用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来管理流媒体服务器或客户端。