Actor2架构解读

Actor

应该叫ActorManager更确切点,它用来注册Actor、启动Proxy和ActorWorker进程。 当你在业务逻辑里定义了几种Actor,比如RoomActor、PlayerActor,我们需要在SwooleServer启动时注册它们。 具体就是在EasySwooleEvent.mainServerCreate方法中添加如下代码。

  1. $actor = Actor::getInstance();
  2. $actor->register(RoomActor::class);
  3. $actor->register(PlayerActor::class);
  4. $actorConf = Config::getInstance()->getConf('ACTOR_SERVER');
  5. $actor->setMachineId($actorConf['MACHINE_ID'])
  6. ->setListenAddress($actorConf['LISTEN_ADDRESS'])
  7. ->setListenPort($actorConf['PORT'])
  8. ->attachServer($server);

其中ListenAddress、ListenPort为Proxy进程的监听地址端口,MachineId为ActorWorker进程的机器码。 MachineId和IP:PORT对应。 attachServer将开启相应数量的Proxy进程,以及前边register的ActorWorker进程。

工作原理

Proxy进程做消息中转,Worker进程做消息分发推送。来看个具体的例子:

游戏中玩家P请求进入房间R,抽象成Actor模型就是PlayerActor需要往RoomActor发送请求加入的命令。 那么这时候需要这样写:

  1. RoomActor::client($node)->send($roomActorId, [
  2. 'user_actor_id' => $userActorId,
  3. 'data' => '其他进入房间的参数'
  4. ])

其中$roomActorId和$userActorId是你事先xxActor::client()->create()出来的。 上面那段代码的意思就是往$roomActorId的RoomActor实例推送了一条$userActorId玩家的UserActor实例要加入房间的消息。 参数$node用来寻址Proxy,它由目标Actor实例的Worker.MachineId决定,在本例中就是$roomActorId被创建在了哪个MachineId的WorkerProcess。 通过$roomActorId中的机器码找到IP:PORT,生成$node。 send时会创建一个协程TcpClient,将消息发送给Proxy,然后Proxy将消息转发(UnixClient)至本机WorkerProcess,WorkerProcess收到消息,推送到具体的Actor实例。 这样就完成了从PlayerActor到RoomActor的请求通讯,RoomActor收到请求消息并处理完成后,向PlayerActor回发处理结果,用的是同样的通讯流程。 如果是单机部署,可以忽略$node参数,因为所有通讯都是在本机进行。 多机的话,需要自己根据业务来实现Actor如何分布和定位。

主要属性

machineId 机器码

proxyNum 启动几个ProxyProcess

listenPort 监听端口

listenAddress 监听ip

AbstractActor

Actor实例的基类,所有业务中用到的Actor都将继承于AbstractActor。例如游戏场景中的房间,你可以:

  1. class RoomActor extends AbstractActor

工作原理

每个Actor实例都维护一份独立的数据和状态,当一个Actor实例通过client()->create()后,会开启协程循环,接收mailbox pop的消息,进而处理业务逻辑,更新自己的数据及状态。具体实现就是__run()这个方法。

静态方法 configure

用来配置ActorCofig,只需要在具体的Actor(如RoomActor)去重写这个方法就行。 关于ActorConfig具体属性可以看下边ActorConfig部分。

几个虚拟方法

以下几个虚拟方法需要在Actor子类中实现,这几个方法被用在__run()中来完成Actor的运行周期。

onStart() 在协程开启前执行,你可以在此进行Actor初始化的一些操作,比如获取房间的基础属性等。

onMessage() 当接收到消息时执行,一个Actor实例的生命周期基本上就是在收消息-处理-发消息,你需要在这里对消息进行解析处理。

onExit() 当接收到退出命令时执行。比如你希望在一个Actor实例退出的时候,同时通知某些关联的其他Actor,可以在此处理。

其他

exit() 用于实例自己退出操作,会向自己发一条退出的命令。

tick()、after() 两个定时器,用于Actor实例的定时任务,比如游戏房间的定时刷怪(tick);掉线后多长时间自动踢出(after)。

static client() 用于创建一个ActorClient来进行对应Actor(实例)的通讯。

ActorClient

Actor通讯客户端,调用xxActor::client()来创建一个ActorClient进行Actor通讯。 上边已经大概讲过了Actor的通讯流程,本质就是TcpClient->Proxy进程->UnixClient->ActorWorker进程->xxActor。 看下它实现了哪些方法:

create() 创建一个xxActor实例,返回actorId,在之后你可以使用这个actorId与此实例进行通讯。

send() 指定actorId,向其发送消息。

exit() 通知xxActor退出指定actorId的实例。

sendAll() 向所有的xxActor实例发送消息。

exitAll() 退出所有xxActor实例。

exist() 当前是否存在指定actorId的xxActor实例。

status() 当前ActorWorker下xxActor的分布状态。

ActorConfig

具体Actor的配置项,比如RoomActor、PlayerActor都有自己的配置。

actorName 一般用类名就可以,注意在同一个服务中这个是不能重复的。

actorClass 在Actor->register()会将对应的类名写入。

workerNum 为Actor开启几个进程,Actor->attachServer()时会根据这个参数为相应Actor启动WorkerNum个Worker进程

ActorNode

上边提到过,xxActor::client($node),这个$node就是ActorNode对象,属性为Ip和Port,用于寻址Proxy,就不多说了。

WorkerConfig

WorkerProcess的配置项,WorkerProcess启动时用到。

workerId worker进程Id,create Actor的时候用于生成actorId

machineId worker进程机器码,create Actor的时候用于生成actorId

trigger 异常触发处理接口

WorkerProcess

Actor的重点在这里,每个注册的Actor(类)会启动相应数量的WorkerProcess。 比如你注册了RoomActor、PlayerActor,workerNum都配置的是3,那么系统将启动3个RoomActor的Worker进程和3个PlayerActor的Worker进程。 每个WorkerProcess维护一个ActorList,你通过client()->create()的Actor将分布在不同Worker进程里,由它的ActorList进行管理。 WorkerProcess通过协程接收client(这个client就是Proxy做转发时的UnixClient)消息,区分消息类型,然后分发给对应的Actor实例。 请仔细阅读下WorkerProcess的源码,它继承于AbstractUnixProcess。

UnixClient

UnixStream Socket,自行了解。Proxy转发消息给本机Actor所使用的Client。

Protocol

数据封包协议。

ProxyCommand

消息命令对象,Actor2将不同类型的消息封装成格式化的命令,最终传给WorkerProcess。 你可以在ActorClient中了解一下方法和命令的对应关系,但这个不需要在业务层去更改。

ProxyConfig

消息代理的配置项。

actorList 注册的actor列表。

machineId 机器码

tempDir 临时目录

trigger 错误触发处理接口

ProxyProcess

Actor->attachServer()会启动proxyNum个ProxyProcess。 用于在Actor实例和WorkerProcess做消息中转。