GRPCを使用したプロセス間通信

今日は、GRPCプロトコルを使用してNETCoreとNETFramework上のアプリケーション間のプロセス間通信を実装する方法について説明したいと思います。皮肉なことに、NET CoreおよびNET5プラットフォームでWCFの代わりとしてMicrosoftによって宣伝されたGRPCは、この場合、NETCoreでのWCFの実装が不完全だったために発生しました。



この記事が、誰かがIPCを編成するためのオプションを検討し、GRPCのような高レベルのソリューションをこの低レベルの側面から見ることができるようになるときに見つかることを願っています。



私の仕事は7年以上にわたり、いわゆる「健康情報化」に関連しています。独自の特徴はありますが、かなり興味深い分野です。それらのいくつかは、圧倒的な量のレガシーテクノロジー(保守主義)と、ほとんどの既存のソリューションへの統合への一定の近さ(1つのメーカーのエコシステムに対するベンダーロック)です。



環境



現在のプロジェクトで、これら2つの機能の組み合わせに遭遇しました。作業を開始し、特定のソフトウェアとハ​​ードウェアの複合体からデータを受信する必要がありました。最初は、すべてが非常に良さそうに見えました。複合体のソフトウェア部分は、実行のためのコマンドを受け入れ、結果をファイルに吐き出すWCFサービスを起動します。さらに、メーカーはSDKに例を提供しています!何がうまくいかない可能性がありますか?すべてが非常に技術的で現代的です。スプリットスティックを使用したASTMはなく、共有フォルダーを介したファイル共有もありません。



しかし、奇妙な理由で、WCFサービスはWSDualHttpBinding、.NET Core 3.1では使用できない二重パイプとバインディングを使用します。これは「大きな」フレームワーク(またはすでに「古い」フレームワーク)でのみ使用できます。この場合、チャネルの二重性はまったく使用されません。それはサービスの説明にあります。バマー!結局のところ、プロジェクトの残りの部分はNET Core上にあり、それを拒否する必要はありません。この「ドライバー」をNETFramework 4.8の個別のアプリケーションとして収集し、何らかの方法でプロセス間のデータフローを整理する必要があります。



プロセス間通信



. , , , , 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



tutorial. GRPC , , IPC - . .





:



  • IpcGrpcSample.CoreClient — NET Core 3.1, RPC
  • IpcGrpcSample.NetServer — NET Framework 4.8, RPC
  • IpcGrpcSample.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);


プロジェクトコードでは、意図的に多くの簡略化が行われています。例外は処理されず、ロギングは正常に実行されず、パラメータはコードにハードコードされます。これは本番環境に対応していませんが、問題を解決するためのテンプレートです。面白かったと思いますので、質問してください!




All Articles