Redola.Rpc 解决了什么问题?
Redola.Rpc 是一个使用 C# 开发的 RPC 框架,代码开源在 GitHub 上。目前版本仅支持 .NET Framework 4.6 以上版本,未来待系统稳健后再考虑移植 .NET Standard 和 .NET Core。
Redola.Rpc 在 0.3.2 版本中,尝试解决几个 RPC 设计问题:
我是谁?(Local Actor)
如何告诉别人我是谁?(Actor Directory)
我提供什么服务?(Service Catalog Provider)
如何告诉别人我提供什么服务?(Service Directory)
我需要的服务在哪里?(Service Discovery)
如何调用该服务?(Service Dynamic Proxy)
如何找到该服务?(Actor Directory)
如何发消息给该服务?(Remote Actor)
Actor 是什么?
Redola 定义的 Actor 模型代表着一个通信节点,使用 ActorIdentity 描述,包括节点类型 Type、节点名称 Name、节点地址 Address、节点端口 Port。
Actor 与 Actor 之间是基于 TCP Socket 通信的,Actor 并不区分 TCP 的 Server/Client 端,它将 Server 和 Client 封装在底层,为上层应用提供更便捷的传输定义和调用接口。Actor 模型提供了面向通道 Channel 的双工通道,可以接收来自对端的消息,也可以发送消息给对端。
Actor 收发的消息是面向二进制数组的,它不关心具体发送的是什么消息,也不关心序列化格式。Actor 使用 ActorFrameHeader 定义传输消息头,Header 携带消息体长度。
Actor 一旦建立连接,生成的 Channel 通道会自动进行 KeepAlive 双向保活机制。通过 Actor 服务发现,可以与任意的 Actor 进行通信,无需再配置对端节点地址和端口。并且,针对相同 Type 的 Actor,还可以实现消息分发的负载均衡功能。
RPC 契约定义
Redola.Rpc 是基于契约模型通信的,使用 Protobuf 2 格式定义 IDL,并通过自动生成工具生成 Contract 契约定义。
例如,下面是定义 ICalcService 服务的 IDL 定义。
package Redola.Rpc.TestContracts;message AddRequest {required int32 X = 10;required int32 Y = 20; }message AddResponse {required int32 Result = 10; }service CalcService {rpc Add (AddRequest) returns (AddResponse); }
上述 IDL 生成的 ICalcService 接口定义为:
public interface ICalcService {AddResponse Add(AddRequest request); }
RPC 消息序列化
Redola.Rpc 选择使用 Protobuf 2 进行消息序列化,默认集成 protobuf-net 类库,稳定使用 protobuf-net v2.0.0.668 版本。
RPC 消息信封
使用 ActorMessageEnvelope 封装消息信封,携带如下信息:
RPC 消息定义
RPC 消息分为 2 类:
InvokeMethodRequest / InvokeMethodResponse 用于定义请求回复模型的方法调用;
InvokeMethodMessage 用于定义请求无回复模型的方法调用;
通常 RPC 消息会包含如下属性信息:
例如,对于 ICalcService 中的 Add 方法:
MethodLocator = "Rodola.Rpc.TestContracts.ICalcService/Add_AddRequest";
MethodArguments = new object[] { new AddRequest(1, 2)};
鉴于 protobuf 本身是面向契约设计的,而 object[] 中的 object 是有不确定性的,并不能具体描述一个契约,则要求每一个 Argument 都需要支持 protobuf 的序列化,传输时系统会携带该 Argument 类型的 AssemblyQualifiedName,在对端通过反射进行反序列化。
Actor Directory 节点目录
Actor Directory 负责注册本地 Local Actor 到注册中心,Local Actor 也可以在 Shutdown 时将自己从注册中心移除掉。
通过 Actor Directory,Local Actor 可以使用 Type 和 Name 进行 Remote Actor 的检索,进而进行 Channel 的建立和通信。
Actor Directory 通过 IActorDirectory 的抽象定义,可以与不同的目录方案进行集成。例如,自实现基于 Actor 的 CenterActorDirectory,使用 XML 配置文件的 LocalXmlFileActorDirectory,使用 Consul 进行中心注册的 ConsulActorDirectory。
使用 Consul 时,实际上是调用了 Consul HTTP API 中的 Agent Register Service 接口 '/v1/agent/service/register',通过指定 ServiceID 和 ServiceName 进行注册。
通过如下 cmd 启动 Consul Server 和 Consul Agent。
consul.exe agent -config-dir "C:\Consul\config\server-01" -bootstrap -ui consul.exe agent -config-dir "C:\Consul\config\client-01" -join 192.168.1.133:7774 -ui
下面为启动本地 Consul 进行测试的配置文件。
server-01.json
{
"datacenter": "dc1",
"data_dir": "C:\\Consul\\data\\server-01",
"log_level": "INFO",
"node_name": "server-01",
"server": true,
"ports": {
"http": 7771,
"rpc": 7772,
"dns": 7773,
"serf_lan": 7774,
"serf_wan": 7775,
"server": 7776} }
client-01.json
{
"datacenter": "dc1",
"data_dir": "C:\\Consul\\data\\client-01",
"log_level": "INFO",
"node_name": "client-01",
"ports": {
"http": 8881,
"rpc": 8882,
"dns": 8883,
"serf_lan": 8884,
"serf_wan": 8885,
"server": 8886} }
Service Catalog Provider 服务提供者
作为 RPC Service 的 Provider 提供方,需要显式定义指定 Contract 的服务实例。例如,下面将不同的服务契约与服务实例进行了注册。
var serviceCatalog = new ServiceCatalogProvider(); serviceCatalog.RegisterService<IHelloService>(new HelloService()); serviceCatalog.RegisterService<ICalcService>(new CalcService()); serviceCatalog.RegisterService<IOrderService>(new OrderService());
实际上,可以通过对于 IServiceCatalogProvider 接口的不同实现,进行不同方式的本地服务发现和注册。例如,可以使用 Attribute 标记服务,通过对 Assembly 进行反射进行服务的实例化。
Service Directory 服务目录
本地服务聚集到 Catalog 中后,系统会将服务逐个注册到 Service Directory 服务目录中,使得其他节点可以检索服务进行使用。
通过 IServiceDirectory 的抽象定义,可以与不同的目录方案进行集成。例如,使用 XML 配置文件的 LocalXmlFileServiceDirectory,使用 Consul 进行中心注册的 ConsulServiceDirectory。
使用 Consul 时,注册服务的 log 如下所示。
当 Redola 将服务注册至 Consul 中后,可通过 Consul 内置的 UI 进行查看。
http://localhost:8881/ui/#/dc1/services
Service Discovery 服务发现
通过 ConsulServiceDiscovery 实现 IServiceDiscovery 服务发现接口,从 Consul 检索指定服务类型的服务。
通过 Postman 测试 GET /v1/catalog/services,得到如下 JSON 数据。
http://localhost:8881/v1/catalog/services
{
"Redola.Rpc.TestContracts.ICalcService": [], "Redola.Rpc.TestContracts.IHelloService": [], "Redola.Rpc.TestContracts.IOrderService": [], "consul": [], "server": [] }
通过 Postman 测试 GET /v1/catalog/service,得到如下 JSON 数据。
http://localhost:8881/v1/catalog/service/Redola.Rpc.TestContracts.ICalcService
[{
"ID": "359e8dfe-262d-6eb7-260c-e6e3ad208a14",
"Node": "client-01",
"Address": "192.168.1.133",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "192.168.1.133",
"wan": "192.168.1.133"}, "NodeMeta": {},
"ServiceID": "redola/server/server-33333/Redola.Rpc.TestContracts.ICalcService", "ServiceName": "Redola.Rpc.TestContracts.ICalcService", "ServiceTags": [],
"ServiceAddress": "localhost",
"ServicePort": 33333, "ServiceEnableTagOverride": true,
"CreateIndex": 2147,
"ModifyIndex": 2151} ]
服务检索方,可通过指定 IServiceLoadBalancingStrategy 的具体实现实施不同的负载均衡策略,默认指定的是 IServiceLoadBalancingStrategy 随机选择。
Service Dynamic Proxy 动态代理
为简化 RPC 调用发起方的封装,通常会使用 Dynamic Proxy 动态代理技术来动态生成给定契约的服务实例,将整体 RPC 的过程透明化。
例如,通过下面的代码来动态生成 ICalcService 的动态代理。
var calcClient = rpcNode.Resolve<ICalcService>();
目前 Redola.Rpc 默认集成了 Castle.Core 中的 Dynamic Proxy 模块,通过对实例方法的 Intercept 拦截进行 RPC 消息的收发处理。
当然,如需集成其他 Dynamic Proxy 类库,可通过 ISeviceProxyGenerator 接口进行方案实现。
Redola.Rpc 类库依赖
Redola.Rpc 当前实现依赖了如下开源类库。
<?xml version="1.0" encoding="utf-8"?><packages><package id="Consul" version="0.7.2.3" targetFramework="net46" /><package id="Cowboy.Sockets" version="1.3.14.0" targetFramework="net46" /><package id="protobuf-net" version="2.0.0.668" targetFramework="net46" /><package id="Castle.Core" version="4.1.0" targetFramework="net46" /><package id="Logrila.Logging" version="1.0.3.0" targetFramework="net46" /><package id="Logrila.Logging.NLogIntegration" version="1.0.3.0" targetFramework="net46" /><package id="NLog" version="4.2.3" targetFramework="net46" /></packages>