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 封装消息信封,携带如下信息:
属性名称 |
属性类型 |
属性描述 |
MessageID |
string |
消息 ID,唯一 ID,通常使用 GUID。 |
MessageTime |
DateTime |
消息产生时间 |
CorrelationID |
string |
如果是 Response 则回填 Request 的 MessageID。 |
CorrelationTime |
DateTime |
如果是 Response 则回填 Request 的 MessageTime。 |
SourceEndpoint |
ActorEndpoint | 发送端节点描述,消息路由使用,默认不需要填写。 |
TargetEndpoint |
ActorEndpoint | 目的端节点描述,消息路由使用,默认不需要填写。 |
MessageType |
string | 消息类型,使用字符串描述。 |
MessageData |
byte[] | 消息体,消息序列化后的二进制数组。 |
RPC 消息定义
RPC 消息分为 2 类:
- InvokeMethodRequest / InvokeMethodResponse 用于定义请求回复模型的方法调用;
- InvokeMethodMessage 用于定义请求无回复模型的方法调用;
通常 RPC 消息会包含如下属性信息:
属性名称 |
属性类型 |
属性描述 |
MethodLocator |
string |
RPC 方法描述,使用字符串描述。 |
MethodArguments |
object[] |
RPC 方法的入参,object 对象数组。 |
例如,对于 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>