使用GRPC进行进程间通信

今天,我想谈谈我们使用GRPC协议在NET Core和NET Framework上的应用程序之间实现进程间通信的方式。具有讽刺意味的是,在我们的案例中,Microsoft推广GRPC替代WCF在其NET Core和NET5平台上发生的原因恰恰是由于WCF在NET Core中的实现不完整。



我希望当有人考虑组织IPC的选项时能找到这篇文章,并允许您从低层次的角度来研究GRPC这样的高级解决方案。



七年来,我的工作一直与所谓的“健康信息化”相关。尽管它具有自己的特征,但是这是一个非常有趣的领域。其中一些是压倒性的遗留技术(保守性),并且在大多数现有解决方案中具有一定的集成度(厂商锁定在一个制造商的生态系统上)。



语境



在当前项目中,我们遇到了这两个功能的组合:我们需要启动工作并从特定的软件和硬件联合体接收数据。最初,一切看起来都很好:该组合系统的软件部分提供了WCF服务,该服务接受执行命令并将结果吐入文件中。此外,制造商还提供了带有示例的SDK!可能出什么问题了?一切都是相当技术和现代的。没有带有分割棒的ASTM,甚至没有通过共享文件夹共享文件的功能。



但是出于某些奇怪的原因,WCF服务使用双工通道和绑定WSDualHttpBinding,而在.NET Core 3.1中不可用,而仅在“大”框架(或已经在“旧”框架中)使用。在这种情况下,不会以任何方式使用通道的双工!只是在服务描述中。mm!毕竟,该项目的其余部分都位于NET Core上,因此没有放弃它的愿望。我们将必须将此“驱动程序”收集为NET Framework 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