本文主要介绍GRPC 一元RPC和双向流GRPC的简单代码实现。
一、引入Nuget包
- 服务端:
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:服务对象的构建;
- 客户端:
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方法,服务端、客户端分别通过请求流和响应流来处理数据;
-
一元:
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);
} -
双向流:
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实现
- 一元:
-
服务端:
public class SingleServer : SingleService.SingleServiceBase // SingleService.SingleServiceBase就是从proto编译出来的
{
public override TaskSingleCommand(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中定义的方法,给一个请求参数,返回一个相应参数 // 相应业务 }}
- 双向流:
-
服务端:
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为发送一条请求,接收一条响应,本身没有链接状态,不需要处理断连。
- 客户端:
ConfigureKestrel中增加
-
listenOptions.Protocols = HttpProtocols.Http2;
-
listenOptions.KeepAliveTime = TimeSpan.FromSecond(30); // 30s没通信流量就发ping
-
listenOptions.KeepAliveTimeout = TimeSpan.FromSecond(10); // ping后10s无响应就判死
下次调用时会报出RpcExcption异常;
- 服务端:
- channel = GrpcChannel.ForAddress(addr, new GrpcChannelOptions{
KeepAliveTime = TimeSpan.FromSecond(30);
KeepAliveTimeout = TimeSpan.FromSecond(10);
});
下次调用时会报出RpcExcption异常,ReadAllAsync()抛出异常就算断连,等待3秒钟后重新创建channel和client对象,直至进程结束;
六、GRPC数据读写逻辑:
-
双方固定request.Id作为单次发送的表示,即在proto的message中加上int64的id;
-
服务端,调用GRPC发送流进行发送,传递带有id的message后,在线程安全的字典中加入id(key)、TaskCompletionSource<响应message> tcs,并return等待await tcs.Task.ConfigureAwait();在另外的地方循环读取解决,匹配响应的id,在字典中找到对应tcs就给它TrySetResult,左边请求调用就return回去;
-
客户端,不进行处理;