Komunikasi antarproses menggunakan GRPC

Hari ini saya ingin berbicara tentang cara kami mengimplementasikan komunikasi antarproses antara aplikasi pada NET Core dan NET Framework menggunakan protokol GRPC. Ironisnya adalah GRPC, yang dipromosikan oleh Microsoft sebagai pengganti WCF pada platform NET Core dan NET5 mereka, dalam kasus kami terjadi justru karena implementasi WCF yang tidak lengkap di NET Core.



Saya harap artikel ini akan ditemukan ketika seseorang mempertimbangkan opsi untuk mengatur IPC dan memungkinkan Anda untuk melihat solusi tingkat tinggi seperti GRPC dari sisi tingkat rendah ini.



Selama lebih dari 7 tahun, aktivitas kerja saya dikaitkan dengan apa yang disebut "informatisasi kesehatan". Ini kawasan yang cukup menarik, meski memiliki ciri khas tersendiri. Beberapa di antaranya adalah banyaknya teknologi warisan (konservatisme) dan kedekatan tertentu dengan integrasi di sebagian besar solusi yang ada (vendor-lock pada ekosistem satu pabrikan).



Konteks



Kami menemukan kombinasi dari dua fitur ini pada proyek saat ini: kami perlu memulai pekerjaan dan menerima data dari kompleks perangkat lunak dan perangkat keras tertentu. Pada awalnya, semuanya terlihat sangat bagus: bagian perangkat lunak dari kompleks ini menampilkan layanan WCF, yang menerima perintah untuk dieksekusi dan memasukkan hasilnya ke dalam file. Selain itu, pabrikan memberikan contoh kepada SDK! Apa yang salah? Semuanya cukup berteknologi dan modern. Tidak ada ASTM dengan tongkat terpisah, bahkan tidak berbagi file melalui folder bersama.



Tetapi untuk beberapa alasan aneh, layanan WCF menggunakan pipa dupleks dan binding WSDualHttpBindingyang tidak tersedia di bawah .NET Core 3.1, hanya dalam kerangka "besar" (atau sudah dalam kerangka "lama"?). Dalam hal ini, dupleksitas saluran tidak digunakan dengan cara apa pun! Itu hanya di deskripsi layanan. Kekecewaan! Bagaimanapun, sisa proyek tinggal di NET Core dan tidak ada keinginan untuk menolaknya. Kita harus mengumpulkan "driver" ini sebagai aplikasi terpisah pada NET Framework 4.8 dan mencoba mengatur aliran data antar proses.



Komunikasi antarproses



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


Banyak penyederhanaan telah dibuat dalam kode proyek dengan sengaja - pengecualian tidak ditangani, logging tidak dilakukan secara normal, parameter di-hardcode ke dalam kode. Ini belum siap produksi, tetapi template untuk memecahkan masalah. Saya harap itu menarik, ajukan pertanyaan!




All Articles