Heute möchte ich über unsere Art der Implementierung der Interprozesskommunikation zwischen Anwendungen auf NET Core und NET Framework mithilfe des GRPC-Protokolls sprechen. Die Ironie ist, dass GRPC, das von Microsoft als Ersatz für WCF auf ihren NET Core- und NET5-Plattformen beworben wurde, in unserem Fall genau aufgrund einer unvollständigen Implementierung von WCF in NET Core geschah.
Ich hoffe, dieser Artikel wird gefunden, wenn jemand die Optionen für die Organisation von IPC in Betracht zieht und es Ihnen ermöglicht, eine so übergeordnete Lösung wie GRPC von dieser untergeordneten Seite aus zu betrachten.
Seit mehr als 7 Jahren ist meine Arbeitstätigkeit mit der sogenannten "Gesundheitsinformatisierung" verbunden. Dies ist ein ziemlich interessantes Gebiet, obwohl es seine eigenen Merkmale hat. Einige davon sind die überwältigende Menge an Legacy-Technologien (Konservatismus) und eine gewisse Nähe zur Integration in die meisten vorhandenen Lösungen (Anbietersperre für das Ökosystem eines Herstellers).
Kontext
Beim aktuellen Projekt sind wir auf eine Kombination dieser beiden Funktionen gestoßen: Wir mussten die Arbeit einleiten und Daten von einem bestimmten Software- und Hardwarekomplex empfangen. Anfangs sah alles sehr gut aus: Der Softwareteil des Komplexes ruft einen WCF-Dienst auf, der Befehle zur Ausführung akzeptiert und die Ergebnisse in eine Datei spuckt. Darüber hinaus stellt der Hersteller dem SDK Beispiele zur Verfügung! Was könnte schiefgehen? Alles ist sehr technologisch und modern. Kein ASTM mit Split-Sticks, nicht einmal Dateifreigabe über einen freigegebenen Ordner.
Aus irgendeinem seltsamen Grund verwendet der WCF-Dienst Duplexkanäle und -bindungen WSDualHttpBinding, die unter .NET Core 3.1 nicht nur im "großen" Framework (oder bereits im "alten"?) Verfügbar sind. In diesem Fall wird die Duplexität der Kanäle in keiner Weise genutzt! Es ist nur in der Beschreibung des Dienstes. Schade! Immerhin lebt der Rest des Projekts von NET Core und es besteht kein Wunsch, es abzulehnen. Wir müssen diesen "Treiber" als separate Anwendung in NET Framework 4.8 sammeln und irgendwie versuchen, den Datenfluss zwischen Prozessen zu organisieren.
Interprozesskommunikation
. , , , , 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);
Der Projektcode wurde absichtlich stark vereinfacht - Ausnahmen werden nicht behandelt, die Protokollierung wird nicht normal durchgeführt, Parameter werden fest in den Code codiert. Dies ist nicht produktionsbereit, sondern eine Vorlage zur Lösung von Problemen. Ich hoffe es war interessant, Fragen stellen!