Today I want to talk about our way of implementing interprocess communication between applications on NET Core and NET Framework using the GRPC protocol. The irony is that GRPC, promoted by Microsoft as a replacement for WCF on their NET Core and NET5 platforms, in our case happened precisely because of an incomplete implementation of WCF in NET Core.
I hope this article will be found when someone will consider the options for organizing IPC and will allow you to look at such a high-level solution like GRPC from this low-level side.
For more than 7 years my work activity has been associated with what is called "health informatization". This is a rather interesting area, although it has its own characteristics. Some of them are the overwhelming amount of legacy technologies (conservatism) and a certain closeness to integration in most existing solutions (vendor-lock on the ecosystem of one manufacturer).
Context
We encountered a combination of these two features on the current project: we needed to initiate work and receive data from a certain software and hardware complex. At first, everything looked very good: the software part of the complex lifts the WCF service, which accepts commands for execution and spits the results into a file. Moreover, the manufacturer provides SDK with examples! What could go wrong? Everything is quite technological and modern. No ASTM with split sticks, not even file sharing via a shared folder.
But for some strange reason, the WCF service uses duplex pipes and bindings WSDualHttpBinding
that are not available under .NET Core 3.1, only in the "big" framework (or already in the "old"?). In this case, the duplexity of the channels is not used in any way! It's just in the description of the service. Bummer! After all, the rest of the project lives on NET Core and there is no desire to give it up. We'll have to collect this "driver" as a separate application on NET Framework 4.8 and somehow try to organize the flow of data between processes.
Interprocess communication
. , , , , 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);
Many simplifications have been made in the project code on purpose - exceptions are not handled, logging is not performed normally, parameters are hardcoded into the code. This is not production-ready, but a template for solving problems. I hope it was interesting, ask questions!