在现代分布式系统中,高效的通信机制是确保应用性能和可扩展性的关键。gRPC(Google Remote Procedure Call)作为一种高性能、开源和通用的 RPC 框架,已经成为越来越多开发者的选择。本文将带你从零开始了解 gRPC 的基本概念,并通过实际示例展示如何在 .NET 应用程序中使用 gRPC 实现服务间的通信。
gRPC 简介
在 gRPC 中,客户端应用程序可以直接调用不同机器上的服务器应用程序上的方法,就好像它是本地对象一样,这使得您更容易创建分布式应用程序和服务。与许多 RPC 系统一样,gRPC 基于定义服务的理念,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现此接口并运行 gRPC 服务器以处理客户端调用。在客户端,客户端具有一个存根(在某些语言中称为客户端),它提供与服务器相同的 方法。
gRPC 客户端和服务器可以在各种环境中运行并相互通信 - 从 Google 内部服务器到您自己的桌面 - 并且可以用 gRPC 支持的任何语言编写。因此,例如,您可以轻松地在 Java 中创建一个 gRPC 服务器,并在 Go、Python 或 Ruby 中创建客户端。此外,最新的 Google API 将具有其接口的 gRPC 版本,使您可以轻松地将 Google 功能构建到您的应用程序中。
为什么选择 gRPC
高效通信:gRPC 使用 HTTP/2 协议进行通信,支持二进制流数据,传输效率更高。
强类型消息:基于 Protocol Buffers(protobuf),定义服务接口和消息类型,确保数据的序列化和反序列化都是类型安全的。
支持多种语言:除了 .NET,gRPC 还支持多种编程语言,如 Java、Python、Go 等,方便跨语言开发。
易于使用:通过简单的 .proto 文件定义服务,生成客户端和服务端代码,简化开发流程。
性能优势:相比传统的 REST API,gRPC 提供了更低的延迟和更高的吞吐量。
双向流支持:gRPC 支持客户端到服务器、服务器到客户端以及双向流通信,适用于复杂的场景。
安全性:内置对 TLS 的支持,确保通信的安全性。
简洁性:使用 protobuf 定义数据结构,代码更简洁,易于维护。
上机实战
一、环境准备
1. 新建三个项目,分别是表示:Client(客户端),Service(服务端),Protocol(通信协议)。
2. Protocol项目安装nuget包,Grpc.Core,Google.Protobuf,Grpc.Tools。
Grpc.Core:gRPC核心
Google.Protobuf:序列化和反序列化
Grpc.Tools:Protobuf协议编译
3. Protocol项目中添加proto协议后,在属性选项中,Build Action 改成 Protobuf compiler
如下图:
二、定义协议
在Protcol项目中,新建一个文本,更改扩展名为:proto,如上:test.proto。添加协议内容如下:
syntax = "proto3";
option objc_class_prefix = "PB3";
package test_pb;
//如果需要引用其它的协议
//import "Test/Test.ext.proto";
//服务类
service TestRPC {
// Client -> Service
rpc Test(TestNullReq)returns(TestRes);
//Servier -> Client
rpc Subscribe(TestNullReq)returns(stream TestRes);
}
//枚举示例
enum OperateType
{
NONE = 0;
Move=1;
Scan=2;
}
//空对象示例
message TestNullReq
{
}
//多数据类型示例
message TestRes
{
//int
int32 id=1;
//long
int64 id2=2;
// float
float id3=3;
// string
string name = 4;
// enum
OperateType type = 5;
// RepeatedField<string> 一定不为null,相当于 array
repeated string names = 6;
// ByteString 一定不为null,相当于 byte[]
bytes datas = 7;
// bool
bool isEnabled = 8;
//MapField<string,string> 一定不为null, 相当于 Dictionary<string,string>
map<string, string> dict = 9;
}
协议添加完成后,正常编译项目即可。
三、添加服务端
/// <summary>
/// 消息处理
/// </summary>
public class MessageWriter<T> where T : IMessage
{
private ConcurrentQueue<T> _messageQueue = new ConcurrentQueue<T>();
private AutoResetEvent _sendEvent = new AutoResetEvent(false);
/// <summary>
/// 是否可用
/// </summary>
public bool IsEnabled { get; private set; } = true;
/// <summary>
/// 准备写数据
/// </summary>
public async Task PreparingToWriteAsync(IServerStreamWriter<T> streamWriter)
{
var task = await Task.Factory.StartNew(async () =>
{
while (IsEnabled)
{
while (_messageQueue.Count > 0)
{
if (_messageQueue.TryDequeue(out T message))
{
try
{
await streamWriter.WriteAsync(message);
}
catch
{
IsEnabled = false;
break;
}
}
}
_sendEvent.WaitOne();
}
}, TaskCreationOptions.LongRunning);
task.Wait();
}
/// <summary>
/// 将需要发送的数据进行入队
/// </summary>
/// <param name="message"></param>
public void Send(T message)
{
_messageQueue.Enqueue(message);
_sendEvent.Set();
}
}
//添加协议处理类
public class TestGrpcService : TestRPC.TestRPCBase
{
//消息推送器
MessageWriter<TestRes> messageWriter;
//Client -> Service
public override Task<TestRes> Test(TestNullReq request, ServerCallContext context)
{
var res = new TestRes()
{
Name = "测试",
Id = this.GetHashCode(),
};
return Task.FromResult(res);
}
//Service -> Client
public override async Task Subscribe(TestNullReq request, IServerStreamWriter<TestRes> responseStream, ServerCallContext context)
{
//演示代码比较简单,这里需要处理多次,多终端订阅的情况
//订阅了就初始化消息推送器
messageWriter = new MessageWriter<TestRes>();
await messageWriter.PreparingToWriteAsync(responseStream);
}
/// <summary>
/// 测试推送消息给客户端
/// </summary>
/// <param name="msg"></param>
public void PushToClient(TestRes msg)
{
messageWriter?.Send(msg);
}
}
//主窗口中,添加测试代码
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Connect("127.0.0.1",5089);
}
//开始监听
public void Connect(string ip, int port)
{
var testGrpcService = new TestGrpcService();
Server server = new Server
{
Services =
{
TestRPC.BindService(testGrpcService),
},
Ports = { new ServerPort(ip, port, ServerCredentials.Insecure) }
};
server.Start();
}
}
四、添加客户端
//添加客户端类
public class TestGrpcClient : TestRPC.TestRPCClient
{
public TestGrpcClient(ChannelBase channel):base(channel)
{
}
}
//添加测试代码
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Connect("127.0.0.1", 5089);
}
TestGrpcClient testGrpcClient;
private async Task Connect(string ip, int port)
{
var channnel = new Channel($"{ip}:{port}", ChannelCredentials.Insecure);
await channnel.ConnectAsync(DateTime.UtcNow + TimeSpan.FromSeconds(10));
testGrpcClient = new TestGrpcClient(channnel);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// 常规调用
var res = testGrpcClient.Test(new TestPb.TestNullReq());
//var res = testGrpcClient.TestAsync(new TestPb.TestNullReq());
DataContext = $"{res.Name}:{res.Id}";
//注册服务端回调
//using (var call = testGrpcClient.Subscribe(new TestNullReq()))
//{
//var responseStream = call.ResponseStream;
//while (await responseStream.MoveNext())
//{
//var res = responseStream.Current;
//DataContext = $"{res.Name}:{res.Id}";
//}
//}
}
//从byte数组解析协议
//TestRes.Parser.ParseFrom()
//proto转byte,方便存放本地文件
//res.ToByteArray();
}
以上示例就可以完成客户端与服务端双方向的通信了。