Aujourd'hui, je veux parler de notre façon de mettre en œuvre la communication interprocessus entre les applications sur NET Core et NET Framework en utilisant le protocole GRPC. L'ironie est que GRPC, promu par Microsoft en remplacement de WCF sur leurs plates-formes NET Core et NET5, s'est produit dans notre cas précisément en raison d'une implémentation incomplète de WCF dans NET Core.
J'espère que cet article sera trouvé lorsque quelqu'un examinera les options d'organisation de l'IPC et vous permettra d'examiner une solution de haut niveau comme GRPC de ce côté de bas niveau.
Depuis plus de 7 ans, mon activité professionnelle est associée à ce qu'on appelle «l'informatisation de la santé». C'est un domaine assez intéressant, bien qu'il ait ses propres caractéristiques. Certains d'entre eux sont la quantité écrasante de technologies héritées (conservatisme) et une certaine proximité de l'intégration dans la plupart des solutions existantes (verrouillage du fournisseur sur l'écosystème d'un fabricant).
Le contexte
Nous avons rencontré une combinaison de ces deux fonctionnalités sur le projet en cours: nous devions lancer le travail et recevoir des données d'un certain complexe logiciel et matériel. Au début, tout avait l'air très bien: la partie logicielle du complexe fait apparaître un service WCF, qui accepte les commandes pour l'exécution et crache les résultats dans un fichier. De plus, le fabricant fournit au SDK des exemples! Qu'est-ce qui pourrait mal se passer? Tout est assez technologique et moderne. Pas d'ASTM avec des bâtons séparés, pas même de partage de fichiers via un dossier partagé.
Mais pour une raison étrange, le service WCF utilise des canaux et des liaisons duplex WSDualHttpBinding
qui ne sont pas disponibles sous .NET Core 3.1, uniquement dans le "grand" framework (ou déjà dans le "vieux"?). Dans ce cas, la duplexité des canaux n'est en aucun cas utilisée! C'est juste dans la description du service. Bummer! Après tout, le reste du projet vit sur NET Core et il n'y a aucune envie d'y renoncer. Nous devrons collecter ce "pilote" en tant qu'application distincte sur NET Framework 4.8 et essayer en quelque sorte d'organiser le flux de données entre les processus.
Communication interprocessus
. , , , , 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);
De nombreuses simplifications ont été faites exprès dans le code du projet - les exceptions ne sont pas gérées, la journalisation n'est pas effectuée normalement, les paramètres sont codés en dur dans le code. Ce n'est pas prêt pour la production, mais un modèle pour résoudre les problèmes. J'espère que c'était intéressant, posez des questions!