C# GRPC 一元与双向流

本文主要介绍GRPC 一元RPC和双向流GRPC的简单代码实现。

一、引入Nuget包

  1. 服务端:

Grpc.Tools:将xxx.proto文件编译生成xxx.cs文件;仅在构建期需要,注意在csproj中进行配置<PackageReference Include="Grpc.Tools"><PrivateAssets>all;注意配置<Protobuf Include="xxx.proto" GrpcServices="Server"/>

Google.Protobuf:xxx.cs里面的使用的方法、对象都是这个包的;

Grpc.AspNetCore.Server:服务端的配置;

Grpc.Core.Application:服务对象的构建;

  1. 客户端:

Grpc.Tools:同上;注意配置<Protobuf Include="xxx.proto" GrpcServices="Client"/>

Google.Protobuf:同上;

Grpc.Net.client:创建client对象用的包;

二、proto

proto文件定义了需要实现的客户端\服务端的Service对象、代表class的message对象、需要实现的方法;

一元RPC,在proto定义服务与具体方法,在服务端继承服务、实现方法,客户端处可直接调用该方法;

双向流GRPC,在proto定义服务与Connect方法,在服务端继承服务、实现Connect方法,服务端、客户端分别通过请求流和响应流来处理数据;

  1. 一元:

    syntax = "proto3";
    option csharp_namespace = "xxxa";
    package singlepackage;

    message SingleRequest
    {
    int64 id = 1;
    string str = 2;
    }

    message SingleResponse
    {
    int64 id = 1;
    string str = 2;
    }

    service SingleService
    {
    rpc SingleCommand(SingleRequest) returns (SingleResponse);
    }

  2. 双向流:

    syntax = "proto3";
    option csharp_namespace = "xxxa";
    package singlepackage;

    message TransValue
    {
    oneof value
    {
    bool bool_value = 1;
    double double_value = 1;
    int64 int64_value = 1;
    string string_value = 1; // grpc也就这四种数据类型了
    }
    }

    message TransResponse
    {
    int64 id = 1;
    TransValue trans_value = 2;
    }

    message TransRequest
    {
    int64 id = 1;
    TransValue trans_value = 2;
    }

    service TransService{
    rpc Connect(stream TransRequest) returns (stream TransResponse);
    }

三、服务端的服务配置

复制代码
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>{
    string url = "...:";  // 服务端的ip和端口
    Uri uri = new Uri(url);
    options.ListenAnyIP(uri.Port, listenOptions => 
        listenOptions.Protocols = HttpProtocols.Http2;
        listenOptions.KeepAliveTime = TimeSpan.FromSecond(30);  // 30s没通信流量就发ping
        listenOptions.KeepAliveTimeout = TimeSpan.FromSecond(10); // ping后10s无响应就判死
    );
});  // Microsoft自带的Kestrel服务器

builder.Services.AddGrpc(options => {
    options.MaxReceiveMessageSize = 4096;
    options.MaxSendMessageSize = 4096;
});

IHost host = builder.Build();
(host as WebApplication).MapGrpcService<xxxServer>();  // xxxServer后面其实现
await host.StartAsync();

四、Service实现

  1. 一元:
  • 服务端:

    public class SingleServer : SingleService.SingleServiceBase // SingleService.SingleServiceBase就是从proto编译出来的
    {
    public override Task SingleCommand(SingleRequest request)
    {
    // 处理业务
    return Task.From(new SingleResponse(){ ID = 123, Str = "xxx"});
    }
    }

  • 客户端:

    public class SingleClient
    {
    private SingleService.SingleServiceClient _client;

    复制代码
      public SingleClient()
      {
          GrpcChannel channel = GrpcChannel.ForAddress("...:");  // 服务端的ip和端口,和TCP是一样的,客户端这边不用配置ip和端口,直接连接服务端的ip和端口就行
          _client = new SingleService.SingleServiceClient(channel);
      }
    
      public void Send()
      {
          SingleResponse response = _client.SingleCommand(new SingleRequest(){ ID = 123, Str = "xxx"});  // 调用proto中定义的方法,给一个请求参数,返回一个相应参数
          // 相应业务
      }

    }

  1. 双向流:
  • 服务端:

    public class TransServer : TransService.TransServiceBase // TransService.TransServiceBase就是从proto编译出来的
    {
    private int _id;

    复制代码
      private ConcurrentDictionary<int,TaskCompletionSource<TransResponse>> _dic = [];
    
      private IAysncStreamReader<TransResponse> _responseReader;
    
      private IAysncStreamReader<TransRequest> _requestReader;
    
      public override async Task Connect(IAysncStreamReader<TransResponse> responseReader, IAysncStreamReader<TransRequest> requestReader)
      {
          _requestReader = requestReader;
          _responseReader = responseReader;
          await LoopRead();  // 服务端循环读
      }
    
      public Task<TransResponse> Send()
      {
          TaskCompletionSource<TransResponse> cts = new TaskCompletionSource<TransResponse>();
          id++;
          _dic[id] = cts;
          try
          {
              return await cts.Task.ConfigureAwait(false);
          }
          finally
          {
              _dic.Remove(id);
          }
      }
    
      private async Task LoopRead()
      {
          await foreach (TransResponse resp in _requestReader.ReadAllAsync())
          {
              if (_dic.ContainsKey(resp.Id))
              {
                  // 处理业务
                  _dic.TrySetResult(resp);
              }
          }
      }

    }

  • 客户端:

    public class TransClient
    {
    private int _id;

    复制代码
      private IAysncStreamReader<TransResponse> _responseReader;
    
      private IClientStreamWriter<TransRequest> _requestReader;
    
      public TransClient()
      {
          Create();
      }
    
      public void Create()
      {
          GrpcChannel channel = GrpcChannel.ForAddress("...:");  // // 服务端的ip和端口,和TCP是一样的,客户端这边不用配置ip和端口,直接连接服务端的ip和端口就行
          _client = new SingleService.SingleServiceClient(channel, new GrpcChannelOptions{
                  KeepAliveTime = TimeSpan.FromSecond(30);    // 30s没通信流量就发ping
                  KeepAliveTimeout = TimeSpan.FromSecond(10); // ping后10s无响应就判死 与客户端那边是独立的,各自发各自的ping,当自己发通信断连后,下一次自己的主动请求抛RpcException异常;
          });
    
          AsyncDuplexStreamingCall<TransRequest, TransResponse> call = this._client.Connect();  // 比一元多的环节
          _requestReader = call.ResponseReader;
          _responseReader = call.RequestReader;  // 这里注意下,对于客户端来说,主动发送时发送response,response流才是发送流,因此这里需要调转一下
          await LoopRead();  // 客户端也需要循环读,且作为主动发起请求的客户端,不需要接收后再做相应,因此LoopRead处理业务后退出即可
      }
    
      public Task<TransResponse> Send()
      {
          _responseReader.Send(new TransResponse() { Id = _id ...});
      }
    
      private async Task LoopRead()
      {
          try
          {
              await foreach (TransResponse resp in _requestReader.ReadAllAsync())
              {
                  // 处理业务
              }
          }
          catch(Exception)
          {
              await Task.Delay(3000);
              Create();  // 此处省略了锁和一些线程同步的内容,意思就是出现异常就等三秒,然后重新连接
          }
      }

    }

五、断连处理

一元RPC为发送一条请求,接收一条响应,本身没有链接状态,不需要处理断连。

  1. 客户端:

ConfigureKestrel中增加

  • listenOptions.Protocols = HttpProtocols.Http2;

  • listenOptions.KeepAliveTime = TimeSpan.FromSecond(30); // 30s没通信流量就发ping

  • listenOptions.KeepAliveTimeout = TimeSpan.FromSecond(10); // ping后10s无响应就判死

下次调用时会报出RpcExcption异常;

  1. 服务端:
  • channel = GrpcChannel.ForAddress(addr, new GrpcChannelOptions{

KeepAliveTime = TimeSpan.FromSecond(30);

KeepAliveTimeout = TimeSpan.FromSecond(10);

});

下次调用时会报出RpcExcption异常,ReadAllAsync()抛出异常就算断连,等待3秒钟后重新创建channel和client对象,直至进程结束;

六、GRPC数据读写逻辑:

  1. 双方固定request.Id作为单次发送的表示,即在proto的message中加上int64的id;

  2. 服务端,调用GRPC发送流进行发送,传递带有id的message后,在线程安全的字典中加入id(key)、TaskCompletionSource<响应message> tcs,并return等待await tcs.Task.ConfigureAwait();在另外的地方循环读取解决,匹配响应的id,在字典中找到对应tcs就给它TrySetResult,左边请求调用就return回去;

  3. 客户端,不进行处理;

相关推荐
拂拉氏4 小时前
【知识讲解】 Linux程序替换相关接口讲解
linux·程序替换
huang5791474 小时前
复杂网络中的最短路径搜索算法性能分析3
算法
找方案4 小时前
AI+气象预报:华为盘古大模型如何让天气预报精准到街区
人工智能·算法·机器学习
星源~4 小时前
zephyr-Linux环境下搭建步骤
linux·mcu·嵌入式开发·zephyr
Huangjin007_4 小时前
【Linux 系统篇(二十)】进程(八):进程创建、进程退出
linux·运维·服务器
Yingye Zhu(HPXXZYY)4 小时前
洛谷B4337 [中山市赛 2023] 简单数学题
c++·算法
pen-ai5 小时前
【优化方法】为什么梯度是最陡峭的方向?
人工智能·算法·机器学习·最小二乘法
吴声子夜歌5 小时前
Shell编程——开发规范
linux·运维·shell
个 人 练 习 生5 小时前
数据结构:排序算法详解
c语言·数据结构·经验分享·学习·算法·排序算法