Hoy quiero hablar sobre nuestra forma de implementar la comunicación entre procesos entre aplicaciones en NET Core y NET Framework usando el protocolo GRPC. La ironía es que GRPC, promovido por Microsoft como reemplazo de WCF en sus plataformas NET Core y NET5, en nuestro caso sucedió precisamente por una implementación incompleta de WCF en NET Core.
Espero que este artículo se encuentre cuando alguien considere las opciones para organizar IPC y le permita ver una solución de tan alto nivel como GRPC desde este lado de bajo nivel.
Durante más de 7 años mi actividad laboral ha estado asociada a lo que se denomina "informatización en salud". Esta es una zona bastante interesante, aunque tiene sus propias características. Algunos de ellos son la abrumadora cantidad de tecnologías heredadas (conservadurismo) y cierta proximidad a la integración en la mayoría de las soluciones existentes (bloqueo del proveedor en el ecosistema de un fabricante).
Contexto
Encontramos una combinación de estas dos características en el proyecto actual: necesitábamos iniciar el trabajo y recibir datos de un determinado complejo de software y hardware. Al principio, todo se veía muy bien: la parte de software del complejo muestra un servicio WCF, que acepta comandos para su ejecución y escupe los resultados en un archivo. Además, el fabricante proporciona SDK con ejemplos. ¿Qué puede salir mal? Todo es bastante tecnológico y moderno. Sin ASTM con palos divididos, ni siquiera compartir archivos a través de una carpeta compartida.
Pero por alguna extraña razón, el servicio WCF utiliza enlaces y canales dúplex WSDualHttpBinding, que no están disponibles en .NET Core 3.1, solo en el marco "grande" (¿o ya en el "antiguo"?). En este caso, la duplexidad de los canales no se utiliza de ninguna manera. Solo está en la descripción del servicio. ¡Gorrón! Después de todo, el resto del proyecto vive en NET Core y no hay ningún deseo de rechazarlo. Tendremos que recopilar este "controlador" como una aplicación separada en NET Framework 4.8 y de alguna manera intentar organizar el flujo de datos entre procesos.
Comunicación entre procesos
. , , , , 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.ProtocolGoogle.Protobuf,GrpcGrpc.Tools -
Grpc,Grpc.Core,Microsoft.Extensions.HostingMicrosoft.Extensions.Hosting.WindowsServices. -
Grpc.Net.ClientOneOf— .
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);
Se han realizado muchas simplificaciones en el código del proyecto a propósito: las excepciones no se manejan, el registro no se lleva a cabo normalmente, los parámetros están codificados en el código. Esto no está listo para producción, sino una plantilla para resolver problemas. Espero que haya sido interesante, ¡haz preguntas!