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. 客户端,不进行处理;

相关推荐
qz_Serene18 分钟前
练习算法题:双指针 -cpp
c++·算法·leetcode·1024程序员节
可乐_不渴23 分钟前
【Linux】 项目自动化构建工具makefile&& 版本管理Git&&第一个程序
linux·git·自动化·1024程序员节
Nuanyt1 小时前
Hot 100 刷题笔记 · 数组与哈希专题(3 题)
java·笔记·算法·leetcode·哈希算法·1024程序员节
碧海银沙音频科技研究院1 小时前
基于炬芯平台ATS308X多模型兼容语音识别算法
人工智能·算法·语音识别
西安同步电子1 小时前
时统卡驱动踩坑:Windows 和 Linux 下时间同步的底层差异
linux·stm32·单片机
虚无的纽扣1 小时前
【Linux】线程互斥:多线程改同一个变量,到底会出什么问题
linux·服务器·1024程序员节
qetfw1 小时前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
m0_547486661 小时前
《Linux操作系统基本原理与应用》全套PPT课件2026
linux·linux系统
爱读书的小胖1 小时前
LeetCode 274 H 指数 C 语言解题分析
c语言·算法·leetcode·1024程序员节
basketball6162 小时前
动态规划第一章:引论
算法·动态规划·1024程序员节