ActorProxyComponent
请大家关注我的微博:@NormanLin_BadPixel坏像素
我们看到,这个组件订阅了Start事件。而且是一个异步启动的方法,作用如作者注释的,每10s扫描一次过期的actorproxy进行回收,过期时间是1分钟。代码也很好懂,这里就不解释了。不过从这里我们大概能猜到,这其实就是一个管理所有ActorProxy的组件。后面的Get、Remove方法就不讲了,需要注意的是,当Get方法查询到字典里没有指定Id的ActorProxy时,会根据Id创建一个新的存入字典。
ActorProxy
在看更多代码之前,我们需要搞懂新的东西ActorTask是什么,
ActorTask
我们看到了熟悉的MessageObject,我们在ClientFrameComponent学习笔记里面稍微提到了一下。其实就是一个基类,具体要看其派生类。
ActorRequest跟ActorResponse就是自定义的消息类型。但是我们现在还不知道这个Actor到底什么用,就当它是一个通信消息类型。我们得看MessageObject具体是什么消息才能知道它的作用。
ActorResponse response = (ActorResponse)await this.proxy.RealCall(request, this.proxy.CancellationTokenSource.Token);
通过ActorProxy异步发送请求并且等待回复。我们可以回到ActorProxy了。
ActorProxy
作者对几个属性的注释很详细了。我这里再提一下,RunningTasks跟WaitingTasks定是重要的,两者共同管理Actor消息的发送。而LastSendTime决定了这个ActorProxy被自动释放的时刻。
我们先来看看Start方法。
public async void Start()
{
int appId = await Game.Scene.GetComponent<LocationProxyComponent>().Get(this.Id);
this.Address = Game.Scene.GetComponent<StartConfigComponent>().Get(appId).GetComponent<InnerConfig>().IPEndPoint;
this.UpdateAsync();
}
这里的新家伙太多,我们分个P。LocationProxyComponent学习笔记
我们知道,在往地址服务器里注册新数据的时候,传入的是实体对象的Id。但是ActorProxy是Component,这里是通过this.Id来获取。不要着急,我们先来看看,我们都是怎么添加ActorProxy组件的。
所有的ActorProxy都是通过ActorProxyComponent的Get方法获取的。
public ActorProxy Get(long id)
{
if (this.ActorProxys.TryGetValue(id, out ActorProxy actorProxy))
{
return actorProxy;
}
actorProxy = ComponentFactory.CreateWithId<ActorProxy>(id);
this.ActorProxys[id] = actorProxy;
return actorProxy;
}
而这个方法是指定Id的,所以,我们在创建的时候只要把实体的ID传入就可以了。
private async void UpdateAsync()
{
while (true)
{
ActorTask actorTask = await this.GetAsync();
if (this.IsDisposed)
{
return;
}
try
{
this.RunTask(actorTask);
}
catch (Exception e)
{
Log.Error(e.ToString());
return;
}
}
}
我们看到,在创建一个ActorProxy之后,就会开始异步处理所有针对该ActorProxy的ActorTask任务。
首先是异步获取到对这个ActorProxy的任务,因为有可能这个ActorProxy被自然回收了(10s内没接收到新的消息),在Dispose里面会返回一个空的ActorProxy来结束这个异步操作。所以,在这个时候会判断该对象已经被释放了,不进行任何操作直接返回。
如果有任务,则通过RunTask来处理任务。
当我们要添加一个任务给这个Actor代理时,会先把任务存入等待处理的队列。在尝试从等待队列中获取任务并处理,如果之前的任务失败了并且在重试,则不处理。
在什么情况下我们无法从等待队列中获取到任务呢?
1. 没有获取的需求。this.tcs == null,this.tcs会在GetAsync里面实例化。
2. 等待队列里没有数据。
3. 在执行的任务数量大于等于最大并行执行数。(这里是1)
否则,我们可以从等待队列中获取到任务,并把它压入RunningTasks队列当中。重置this.tcs,并且返回获取到的ActorTask以完成UpdateAsync里面的异步等待。
ActorTask actorTask = await this.GetAsync();
之后,我们就要对这个任务进行处理。
我们看到,会先运行ActorTask的Run方法。在该方法里,会通过发送这个请求的ActorProxy的Id跟消息组成的ActorRequest。发送的方法在ActorProxy.RealCall 里面。快速看一下这个方法
public async Task<IResponse> RealCall(ActorRequest request, CancellationToken cancellationToken)
{
try
{
//Log.Debug($"realcall {MongoHelper.ToJson(request)} {this.Address}");
request.Id = this.Id;
Session session = Game.Scene.GetComponent<NetInnerComponent>().Get(this.Address);
IResponse response = await session.Call(request, cancellationToken);
return response;
}
catch (RpcException e)
{
Log.Error($"{this.Address} {e}");
throw;
}
}
我们看到,是通过NetInnerComponent组件获得与该ActorProxy所在服务器的会话Session,并且发送请求。无论成功失败都会返回一个结果。
如果请求成功,则更新该ActorProxy最后一次通话的时间和一些数据。
// 发送成功
this.LastSendTime = TimeHelper.Now();
this.failTimes = 0;
if (this.WindowSize < MaxWindowSize)
{
++this.WindowSize;
}
this.RunningTasks.Dequeue();
this.AllowGet();
并且会尝试再获取任务处理。如果等待队列还有,并且满足可处理的条件,则会再次进行一次上面的逻辑。
如果请求失败,则要尝试重新发送请求。
this.CancellationTokenSource.Cancel();
重新发送请求这个需求就体现出this.CancellationTokenSource这个的作用了。不知道大家还记不记得,在我们学习Session的时候。每次我们Call一个Rpc请求的时候,RPCId是唯一的(递增)。我们重新发送请求,已经是一个新的请求了,两者的RPCId已经不同了。
而且我们会根据RPCId来注册一个收到结果后的回调方法。这个方法是在发送的时候就注册了,但是如果我们请求失败了,我们并不希望它去调用这个回调。所以,我们调用Seesion.Call的时候传入一个CancellationTokenSource,并且注册了任务取消的回调。也就是移除我们之前注册的收到结果后的回调方法。
Seesion.Call
public Task<IResponse> Call(IRequest request, CancellationToken cancellationToken)
{
uint rpcId = ++RpcId;
...
this.requestCallback[rpcId] = (packetInfo) =>{...}
...
cancellationToken.Register(()=>this.requestCallback.Remove(rpcId));
...
}
之后我对作者的代码进行事无巨细的注释吧。
IResponse response = await task.Run();
// 如果没找到Actor,发送窗口减少为1,重试
if (response.Error == ErrorCode.ERR_NotFoundActor)
{
//移除之前注册的收到回复的回调。
this.CancellationTokenSource.Cancel();
//发送窗口减少为1
this.WindowSize = 1;
//标记失败次数
++this.failTimes;
//讲等待队列里的任务全部压入RunningTasks队列,为后面做准备。
while (this.WaitingTasks.Count > 0)
{
ActorTask actorTask = this.WaitingTasks.Dequeue();
this.RunningTasks.Enqueue(actorTask);
}
//这是一个交换数据的方法,因为我们的等待队列和执行队列的数据结构是Quene
//先进先出,所以这样对调不会对顺序造成改变。
ObjectHelper.Swap(ref this.RunningTasks, ref this.WaitingTasks);
// 失败3次则清空actor发送队列,返回失败
if (this.failTimes > 3)
{
while (this.WaitingTasks.Count > 0)
{
ActorTask actorTask = this.WaitingTasks.Dequeue();
actorTask.RunFail(response.Error);
}
// 失败直接删除actorproxy
Game.Scene.GetComponent<ActorProxyComponent>().Remove(this.Id);
return;
}
// 等待一会再发送
await Game.Scene.GetComponent<TimerComponent>().WaitAsync(this.failTimes * 500);
//重新获取定位信息
int appId = await Game.Scene.GetComponent<LocationProxyComponent>().Get(this.Id);
this.Address = Game.Scene.GetComponent<StartConfigComponent>().Get(appId).GetComponent<InnerConfig>().IPEndPoint;
this.CancellationTokenSource = new CancellationTokenSource();
//重新发送
this.AllowGet();
return;
}
除此之外,我们看到还有两个方法
public void Send(IMessage message)
{
ActorTask task = new ActorTask();
task.message = (MessageObject)message;
task.proxy = this;
this.Add(task);
}
public Task<IResponse> Call(IRequest request)
{
ActorTask task = new ActorTask();
task.message = (MessageObject)request;
task.proxy = this;
task.Tcs = new TaskCompletionSource<IResponse>();
this.Add(task);
return task.Tcs.Task;
}
这两个方法并不是立马发送消息或者立马发送请求,而是把这些动作以待办任务的方式存入等待队列。