ET框架---ActorProxyComponent学习笔记(ActorProxy)

ActorProxyComponent

请大家关注我的微博:@NormanLin_BadPixel坏像素


我们看到,这个组件订阅了Start事件。而且是一个异步启动的方法,作用如作者注释的,每10s扫描一次过期的actorproxy进行回收,过期时间是1分钟。代码也很好懂,这里就不解释了。不过从这里我们大概能猜到,这其实就是一个管理所有ActorProxy的组件。后面的GetRemove方法就不讲了,需要注意的是,当Get方法查询到字典里没有指定Id的ActorProxy时,会根据Id创建一个新的存入字典。

ActorProxy

在看更多代码之前,我们需要搞懂新的东西ActorTask是什么,

ActorTask

我们看到了熟悉的MessageObject,我们在ClientFrameComponent学习笔记里面稍微提到了一下。其实就是一个基类,具体要看其派生类。

ActorRequestActorResponse就是自定义的消息类型。但是我们现在还不知道这个Actor到底什么用,就当它是一个通信消息类型。我们得看MessageObject具体是什么消息才能知道它的作用。

ActorResponse response = (ActorResponse)await this.proxy.RealCall(request, this.proxy.CancellationTokenSource.Token);

通过ActorProxy异步发送请求并且等待回复。我们可以回到ActorProxy了。

ActorProxy

作者对几个属性的注释很详细了。我这里再提一下,RunningTasksWaitingTasks定是重要的,两者共同管理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都是通过ActorProxyComponentGet方法获取的。

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之后,就会开始异步处理所有针对该ActorProxyActorTask任务。

首先是异步获取到对这个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();

之后,我们就要对这个任务进行处理。

我们看到,会先运行ActorTaskRun方法。在该方法里,会通过发送这个请求的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;
}

这两个方法并不是立马发送消息或者立马发送请求,而是把这些动作以待办任务的方式存入等待队列。

猜你喜欢

转载自blog.csdn.net/norman_lin/article/details/79966243