Hoje quero falar sobre nossa maneira de implementar a comunicação entre processos entre aplicativos no NET Core e NET Framework usando o protocolo GRPC. A ironia é que o GRPC, promovido pela Microsoft como substituto do WCF em suas plataformas NET Core e NET5, em nosso caso aconteceu justamente por causa de uma implementação incompleta do WCF no NET Core.
Espero que este artigo seja encontrado quando alguém considerar as opções para organizar o IPC e permitir que você examine uma solução de alto nível como o GRPC desse lado, de baixo nível.
Há mais de 7 anos a minha atividade laboral está associada ao que se denomina "informatização em saúde". Esta é uma área bastante interessante, embora tenha características próprias. Alguns deles são a quantidade avassaladora de tecnologias legadas (conservadorismo) e uma certa proximidade de integração na maioria das soluções existentes (bloqueio do fornecedor no ecossistema de um fabricante).
Contexto
Encontramos uma combinação desses dois recursos no projeto atual: precisávamos iniciar o trabalho e receber dados de um determinado complexo de software e hardware. A princípio, tudo parecia muito bom: a parte de software do complexo traz um serviço WCF, que aceita comandos para execução e cospe os resultados em um arquivo. Além disso, o fabricante fornece exemplos de SDK! O que poderia dar errado? Tudo é bastante tecnológico e moderno. Sem ASTM com varas divididas, nem mesmo compartilhamento de arquivos por meio de uma pasta compartilhada.
Mas, por algum motivo estranho, o serviço WCF usa canais duplex e associações WSDualHttpBinding
que não estão disponíveis no .NET Core 3.1 apenas na estrutura "grande" (ou já na "antiga"?). Neste caso, a duplexidade dos canais não é utilizada de forma alguma! Está apenas na descrição do serviço. Vadio! Afinal, o resto do projeto reside no NET Core e não há desejo de recusá-lo. Teremos que coletar esse "driver" como um aplicativo separado no NET Framework 4.8 e, de alguma forma, tentar organizar o fluxo de dados entre os processos.
Comunicação entre processos
. , , , , tcp-, - RPC . IPC:
- ,
- Windows ( 7 )
- NET Framework NET Core
, , . ?
, . , . , "". , — . , . , "" "". ? , : , , .
. . , , , workaround, . .
GRPC
, , . GRPC. GRPC? , . .
, :
- , — , Unary call
- —
- — , server streaming rpc
- — HTTP/2
- Windows ( 7 ) — ,
- NET Framework NET Core —
- — , protobuf
- —
- —
,
GRPC 5
:
IpcGrpcSample.CoreClient
— NET Core 3.1, RPCIpcGrpcSample.NetServer
— NET Framework 4.8, RPCIpcGrpcSample.Protocol
— , NET Standard 2.0. RPC
NET Framework Properties\AssemblyInfo.cs
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>...</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">...</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">...</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
NuGet!
-
IpcGrpcSample.Protocol
Google.Protobuf
,Grpc
Grpc.Tools
-
Grpc
,Grpc.Core
,Microsoft.Extensions.Hosting
Microsoft.Extensions.Hosting.WindowsServices
. -
Grpc.Net.Client
OneOf
— .
gRPC
GreeterService
? - . . -, .
.proto
IpcGrpcSample.Protocol
. Protobuf- .
//
syntax = "proto3";
// Empty
import "google/protobuf/empty.proto";
//
option csharp_namespace = "IpcGrpcSample.Protocol.Extractor";
// RPC
service ExtractorRpcService {
// ""
rpc Start (google.protobuf.Empty) returns (StartResponse);
}
//
message StartResponse {
bool Success = 1;
}
//
syntax = "proto3";
//
option csharp_namespace = "IpcGrpcSample.Protocol.Thermocycler";
// RPC
service ThermocyclerRpcService {
// server-streaming " ". -,
rpc Start (StartRequest) returns (stream StartResponse);
}
// -
message StartRequest {
// -
string ExperimentName = 1;
// - , " "
int32 CycleCount = 2;
}
//
message StartResponse {
//
int32 CycleNumber = 1;
// oneof - .
// - discriminated union,
oneof Content {
//
PlateRead plate = 2;
//
StatusMessage status = 3;
}
}
message PlateRead {
string ExperimentalData = 1;
}
message StatusMessage {
int32 PlateTemperature = 2;
}
proto- protobuf . csproj :
<ItemGroup>
<Protobuf Include="**\*.proto" />
</ItemGroup>
2020 Hosting NET Core. Program.cs:
class Program
{
static Task Main(string[] args) => CreateHostBuilder(args).Build().RunAsync();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices(services =>
{
services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.SetMinimumLevel(LogLevel.Trace);
loggingBuilder.AddConsole();
});
services.AddTransient<ExtractorServiceImpl>(); // -
services.AddTransient<ThermocyclerServiceImpl>();
services.AddHostedService<GrpcServer>(); // GRPC HostedService
});
}
. () .
— , — . TLS ( ) — ServerCredentials.Insecure
. http/2 — .
internal class GrpcServer : IHostedService
{
private readonly ILogger<GrpcServer> logger;
private readonly Server server;
private readonly ExtractorServiceImpl extractorService;
private readonly ThermocyclerServiceImpl thermocyclerService;
public GrpcServer(ExtractorServiceImpl extractorService, ThermocyclerServiceImpl thermocyclerService, ILogger<GrpcServer> logger)
{
this.logger = logger;
this.extractorService = extractorService;
this.thermocyclerService = thermocyclerService;
var credentials = BuildSSLCredentials(); // .
server = new Server //
{
Ports = { new ServerPort("localhost", 7001, credentials) }, //
Services = //
{
ExtractorRpcService.BindService(this.extractorService),
ThermocyclerRpcService.BindService(this.thermocyclerService)
}
};
}
/// <summary>
///
/// </summary>
private ServerCredentials BuildSSLCredentials()
{
var cert = File.ReadAllText("cert\\server.crt");
var key = File.ReadAllText("cert\\server.key");
var keyCertPair = new KeyCertificatePair(cert, key);
return new SslServerCredentials(new[] { keyCertPair });
}
public Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation(" GRPC ");
server.Start();
logger.LogInformation("GRPC ");
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation(" GRPC ");
await server.ShutdownAsync();
logger.LogInformation("GRPC ");
}
}
!
. :
internal class ExtractorServiceImpl : ExtractorRpcService.ExtractorRpcServiceBase
{
private static bool success = true;
public override Task<StartResponse> Start(Empty request, ServerCallContext context)
{
success = !success;
return Task.FromResult(new StartResponse { Success = success });
}
}
- :
internal class ThermocyclerServiceImpl : ThermocyclerRpcService.ThermocyclerRpcServiceBase
{
private readonly ILogger<ThermocyclerServiceImpl> logger;
public ThermocyclerServiceImpl(ILogger<ThermocyclerServiceImpl> logger)
{
this.logger = logger;
}
public override async Task Start(StartRequest request, IServerStreamWriter<StartResponse> responseStream, ServerCallContext context)
{
logger.LogInformation(" ");
var rand = new Random(42);
for(int i = 1; i <= request.CycleCount; ++i)
{
logger.LogInformation($" {i}");
var plate = new PlateRead { ExperimentalData = $" {request.ExperimentName}, {i} {request.CycleCount}: {rand.Next(100, 500000)}" };
await responseStream.WriteAsync(new StartResponse { CycleNumber = i, Plate = plate });
var status = new StatusMessage { PlateTemperature = rand.Next(25, 95) };
await responseStream.WriteAsync(new StartResponse { CycleNumber = i, Status = status });
await Task.Delay(500);
}
logger.LogInformation(" ");
}
}
. GRPC Ctrl-C
:
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
Hosting starting
info: IpcGrpcSample.NetServer.GrpcServer[0]
GRPC
info: IpcGrpcSample.NetServer.GrpcServer[0]
GRPC
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\user\source\repos\IpcGrpcSample\IpcGrpcSample.NetServer\bin\Debug
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
Hosting started
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
dbug: Microsoft.Extensions.Hosting.Internal.Host[3]
Hosting stopping
info: IpcGrpcSample.NetServer.GrpcServer[0]
GRPC
info: IpcGrpcSample.NetServer.GrpcServer[0]
GRPC
dbug: Microsoft.Extensions.Hosting.Internal.Host[4]
Hosting stopped
: NET Framework, WCF etc. Kestrel!
grpcurl, . NET Core.
NET Core
. .
. gRPC . RPC .
class ExtractorClient
{
private readonly ExtractorRpcService.ExtractorRpcServiceClient client;
public ExtractorClient()
{
//AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); // http/2 TLS
var httpClientHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator //
};
var httpClient = new HttpClient(httpClientHandler);
var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions { HttpClient = httpClient });
client = new ExtractorRpcService.ExtractorRpcServiceClient(channel);
}
public async Task<bool> StartAsync()
{
var response = await client.StartAsync(new Empty());
return response.Success;
}
}
IAsyncEnumerable<>
OneOf<,>
— .
public async IAsyncEnumerable<OneOf<string, int>> StartAsync(string experimentName, int cycleCount)
{
var request = new StartRequest { ExperimentName = experimentName, CycleCount = cycleCount };
using var call = client.Start(request, new CallOptions().WithDeadline(DateTime.MaxValue)); //
while (await call.ResponseStream.MoveNext())
{
var message = call.ResponseStream.Current;
switch (message.ContentCase)
{
case StartResponse.ContentOneofCase.Plate:
yield return message.Plate.ExperimentalData;
break;
case StartResponse.ContentOneofCase.Status:
yield return message.Status.PlateTemperature;
break;
default:
break;
};
}
}
.
HTTP/2 Windows 7
, Windows TLS HTTP/2. , :
server = new Server //
{
Ports = { new ServerPort("localhost", 7001, ServerCredentials.Insecure) }, //
Services = //
{
ExtractorRpcService.BindService(this.extractorService),
ThermocyclerRpcService.BindService(this.thermocyclerService)
}
};
http
, https
. . , http/2:
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
Muitas simplificações foram feitas no código do projeto propositalmente - as exceções não são tratadas, o registro não é executado normalmente, os parâmetros são codificados no código. Isso não está pronto para produção, mas é um modelo para resolver problemas. Espero que tenha sido interessante, tire dúvidas!