在学习本节之前,您必须熟悉RRQM中的TcpRpcParser
解析器与TcpRpcClient
客户端(或其派生类,例如文件传输)的创建,如果您不熟悉,请在下列链接中了解。
RRQMSocket
RRQMBox
安装RRQMSocket.RPC
即可,具体步骤详看链接博客。
VS、Unity安装和使用Nuget包
从下图(图片来源网络)可以看出,序列化是RPC中至关重要的一个环节,可以说,序列化的优劣,会很大程度的影响RPC调用性能。
在RRQMRPC中,内置了四种序列化方式,分别为RRQMBinary
、SystemBinary
、Json
、Xml
。这四种方式的特点,就是其序列化的特点。
RRQMBinary | SystemBinary | Json | Xml | |
---|---|---|---|---|
特点 | 序列化方式速度快,数据量小,但是兼容的数据格式也比较有限。仅支持基础类型、自定义实体类、数组、List、字典 | 保真度高,支持接口,抽象类,object,泛型等类型的序列化,但是需要Serializable的标签,且必须是同一类型(或重写映射图谱) | 兼容性好,可读性强,但是受字符串影响,性能不出众,且数据量受限制 | 兼容性一般,可读性强,同样受字符串影响,性能不出众,且数据量受限制 |
在RRQMRPC中,选择序列化是非常简单的,且序列化方式完全由调用端
决定。
在实际的调用中,通过InvokeOption
的参数指定。
实际上,只需要传入相关参数即可。
InvokeOption invokeOption = new InvokeOption();
invokeOption.SerializationType = RRQMCore.Serialization.SerializationType.RRQMBinary;
//invokeOption.SerializationType = RRQMCore.Serialization.SerializationType.Json;
//invokeOption.SerializationType = RRQMCore.Serialization.SerializationType.SystemBinary;
//invokeOption.SerializationType = RRQMCore.Serialization.SerializationType.Xml;
string returnString = client.Invoke<string>("TestOne", invokeOption, "10");
a).定义
想要实现自定义序列化,必须通过重写序列化选择器,实现SerializeParameter
和DeserializeParameter
函数。如果还想留用预设序列化,请按下代码示例即可。
public class MySerializationSelector: SerializationSelector
{
///
/// 反序列化
///
///
///
///
///
public override object DeserializeParameter(SerializationType serializationType, byte[] parameterBytes, Type parameterType)
{
if (parameterBytes == null)
{
return parameterType.GetDefault();
}
switch (serializationType)
{
case SerializationType.RRQMBinary:
{
return SerializeConvert.RRQMBinaryDeserialize(parameterBytes, 0, parameterType);
}
case SerializationType.SystemBinary:
{
return SerializeConvert.BinaryDeserialize(parameterBytes, 0, parameterBytes.Length);
}
case SerializationType.Json:
{
return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(parameterBytes), parameterType);
}
case SerializationType.Xml:
{
return SerializeConvert.XmlDeserializeFromBytes(parameterBytes, parameterType);
}
case (SerializationType)4:
{
//此处可自行实现
return default;
}
default:
throw new RRQMRPCException("未指定的反序列化方式");
}
}
///
/// 序列化参数
///
///
///
///
public override byte[] SerializeParameter(SerializationType serializationType, object parameter)
{
if (parameter == null)
{
return null;
}
switch (serializationType)
{
case SerializationType.RRQMBinary:
{
return SerializeConvert.RRQMBinarySerialize(parameter, true);
}
case SerializationType.SystemBinary:
{
return SerializeConvert.BinarySerialize(parameter);
}
case SerializationType.Json:
{
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(parameter));
}
case SerializationType.Xml:
{
return SerializeConvert.XmlSerializeToBytes(parameter);
}
case (SerializationType)4:
{
//此处可自行实现
return default;
}
default:
throw new RRQMRPCException("未指定的序列化方式");
}
}
}
然后,因为赋值时是SerializationType
的枚举类型,所以执行强制类型转换即可。
InvokeOption invokeOption = new InvokeOption();
invokeOption.SerializationType = (RRQMCore.Serialization.SerializationType)4;
RRQMRPC的调用状态有三种状态可选,分别为:OnlySend
、WaitSend
、WaitInvoke
。区别是:
OnlySend | WaitSend | WaitInvoke |
---|---|---|
仅发送RPC请求,在TCP底层协议下,能保证发送成功,但是不反馈服务器任何状态,也不会取得返回值、异常等信息。在UDP底层协议下,不保证发送成功,仅仅是具有请求动作而已。 | 发送RPC请求,并且等待收到状态返回,能保证RPC请求顺利到达服务,但是不能得知RPC服务是否成功执行,也不会取得返回值、异常等信息 | 发送RPC请求,且返回所有信息,包括是否成功调用,执行后的返回值或异常等信息。 |
同样的,在InvokeOption中可以直接赋值使用。
InvokeOption invokeOption = new InvokeOption();
invokeOption.FeedbackType = FeedbackType.WaitInvoke;
//invokeOption.FeedbackType = FeedbackType.OnlySend;
//invokeOption.FeedbackType = FeedbackType.WaitSend;
string returnString = client.Invoke<string>("TestOne", invokeOption, "10");
RPC服务是无状态的,即只知道当前服务被调用,但无法得知是被谁调用,这个问题给日志记录、RPC回调等带来了很多麻烦事。但是,RRQMRPC支持调用上下文获取。在上下文中可以获得调用者(
ICaller
)、MethodInvoker
等。
步骤:
RRQMRPC
标签需要传入IncludeCallContext
参数。IServerCallContext
或其派生类。public class GetCallerRpcServer : ServerProvider
{
[RRQMRPC(MethodFlags.IncludeCallContext)]
public string GetCallerID(IServerCallContext callContext)
{
if (callContext.Caller is RpcSocketClient socketClient)
{
return socketClient.ID;
}
return null;
}
[RRQMRPC(MethodFlags.IncludeCallContext)]
public string GetCallerID_2(RpcServerCallContext callContext)
{
if (callContext.Caller is RpcSocketClient socketClient)
{
return socketClient.ID;
}
return null;
}
}
调用RPC,不能无限制等待,必须要有计时器,或者任务取消的功能。
直接对InvokeOption
的Timeout
属性赋值即可,单位为毫秒
。
InvokeOption invokeOption = new InvokeOption();
invokeOption.Timeout = 1000 * 10;//10秒后无反应,则抛出RRQMTimeoutException异常
string returnString = client.Invoke<string>("TestOne", invokeOption, "10");
在RPC调用时,计时器是一个好的选择,但是还不够完美,有时候我们希望能手动终结某个调用任务。这时候,计时器就不堪重任,需要能主动取消任务的功能。熟悉.net的小伙伴都知道,CancellationToken是具备这个功能的。同样的,只需要对InvokeOption
的CancellationToken
赋值即可。
InvokeOption invokeOption = new InvokeOption();
CancellationTokenSource tokenSource = new CancellationTokenSource();
invokeOption.CancellationToken = tokenSource.Token;
//tokenSource.Cancel();//调用时取消任务
string returnString = client.Invoke<string>("TestOne", invokeOption, "10");
实际上7.2的取消任务,仅仅能实现让客户端取消请求,但是服务器并不知道,如果想让服务器也感知任务消息,就必须依托于调用上下文。
public class ElapsedTimeRpcServer : ServerProvider
{
[Description("测试可取消的调用")]
[RRQMRPC(MethodFlags.IncludeCallContext)]
public bool DelayInvoke(IServerCallContext serverCallContext,int tick)//同步服务
{
for (int i = 0; i < tick; i++)
{
Thread.Sleep(100);
if (serverCallContext.TokenSource.IsCancellationRequested)
{
Console.WriteLine("客户端已经取消该任务!");
return false;//实际上在取消时,客户端得不到该值
}
}
return true;
}
}
调用的服务,必须由承载服务的实例
参与完成,但是,实例的不同,其调用结果大相径庭。
例如:在TestInstanceRpcServer服务中定义了
Count
属性和Increment
函数。每次调用Increment
,Count
会递增,然后返回Count
值。如果某个客户端连续调用两次,会得到什么值呢?亦或者,某两个客户端,各调用一次,会得到什么值呢?
public class TestInstanceRpcServer : ServerProvider
{
public int Count {
get; set; }
[RRQMRPC]
public int Increment()//同步服务
{
return ++Count;
}
}
想必你心里已经有了答案,但是,我要告诉你,你都答案是错误的!Error!Error!
因为,答案既是你想的,也不是你想的。
实际上,RRQMRPC中,支持三种服务实例类型,分别为GlobalInstance
、CustomInstance
、NewInstance
,三者的区别如下。
GlobalInstance | CustomInstance | NewInstance |
---|---|---|
全局实例,即所有客户端,调用一个实例,在上述案例中,Count值会一直递增。 | 用户拥有实例,即一个RPC连接调用一个实例,在上述案例中,Count会在同一客户端调用时递增,在不同客户端中重新计数。 | 新实例,即每次调用,都使用新实例,在上述案例中,获得的Count会一直等于1。 |
服务类型,实际实际上还是由InvokeOption设置。
InvokeOption invokeOption = new InvokeOption();
invokeOption.InvokeType = RRQMSocket.RPC.InvokeType.CustomInstance;
//invokeOption.InvokeType = RRQMSocket.RPC.InvokeType.GlobalInstance;
//invokeOption.InvokeType = RRQMSocket.RPC.InvokeType.NewInstance;
string returnString = client.Invoke<string>("TestOne", invokeOption, "10");
默认情况下,客户端获取代理、发现服务是服务器上定义的全部内容。但是有时候我们希望不同的客户端或不同ProxyToken
获取到的服务是不一样的,这时候,就可以在服务端进行判断,然后决定服务筛选。
GetProxyInfo
和GetRegisteredMethodItems
两个函数。class QosTcpRpcParser : TcpRpcParser
{
///
/// 在获取代理时筛选,
/// 仅筛选代理代码功能,并不能决定服务能不能调用
///
///
///
///
public override RpcProxyInfo GetProxyInfo(string proxyToken, ICaller caller)
{
RpcProxyInfo rpcProxy= base.GetProxyInfo(proxyToken, caller);
if (proxyToken.StartsWith("RPC"))
{
RpcProxyInfo proxyInfo = new RpcProxyInfo()
{
AssemblyName=this.NameSpace+".dll",
Status = 1,//1表示成功,2表示失败
Version = this.RPCVersion.ToString()
};
string ser = proxyToken.Replace("RPC", string.Empty);
proxyInfo.Codes = new List<CellCode>(this.Codes.Where(a =>a.CodeType== CodeType.ClassArgs|| a.Name.Contains(ser)));
return proxyInfo;
}
else
{
return new RpcProxyInfo() {
Status = 2, Message = "你不配拥有代理文件" };//1表示成功,2表示失败
}
}
///
/// 在客户端发现服务时调用,
/// 决定该客户端能不能调用某个服务(或服务函数)
///
///
///
///
public override List<MethodItem> GetRegisteredMethodItems(string proxyToken, ICaller caller)
{
if (proxyToken.StartsWith("RPC"))
{
string ser = proxyToken.Replace("RPC", string.Empty);
//全部服务
List<MethodItem> methodItems = this.MethodStore.GetAllMethodItem();
return new List<MethodItem>(methodItems.Where(m => m.ServerName.Contains(ser)));
}
else
{
return new List<MethodItem>();
}
}
}
上述逻辑主要实现,当客户端的
ProxyToken
不是RPC开头时,返回错误消息(你不配拥有代理文件),当客户端的ProxyToken
是RPC开头时,再次筛选服务。例如:当输入RPCElapsedTimeRpcServer时,仅代理ElapsedTimeRpcServer服务。诸如此类。