ACTOR

提供Actor模式支持,助力游戏行业开发。EasySwoole的Actor采用自定义process作为存储载体,以协程作为最小调度单位,利用协程Channel做mail box,而客户端与process之间的通讯,采用UnixSocket实现,并且借助TCP实现分布式的ActorClient,超高并发下也能轻松应对。

Actor如何工作

一般来说有两种策略用来在并发线程中进行通信:共享数据和消息传递。使用共享数据方式的并发编程面临的最大的一个问题就是数据条件竞争,当两个实例需要访问同一个数据时,为了保证数据的一致性,通常需要为数据加锁,而Actor模型采用消息传递机制来避免数据竞争,无需复杂的加锁操作,各个实例只需要关注自身的状态以及处理收到的消息。

Actor是完全面向对象、无锁、异步、实例隔离、分布式的并发开发模式。Actor实例之间互相隔离,Actor实例拥有自己独立的状态,各个Actor之间不能直接访问对方的状态,需要通过消息投递机制来通知对方改变状态。由于每个实例的状态是独立的,没有数据被共享,所以不会发生数据竞争,从而避免了并发下的加锁问题。

举一个游戏场景的例子,在一个游戏房间中,有5个玩家,每个玩家都是一个PlayerActor,拥有自己的属性,比如角色ID,昵称,当前血量,攻击力等,游戏房间本身也是一个RoomActor,房间也拥有属性,比如当前在线的玩家,当前场景的怪物数量,怪物血量等。此时玩家A攻击某个怪物,则PlayerActor-A向RoomActor发送一个攻击怪物的指令,RoomActor经过计算,得出玩家A对怪物的伤害值,并给房间内的所有PlayerActor发送一个消息(玩家A攻击怪物A,造成175点伤害,怪物A剩余血量1200点),类似此过程,每个PlayerActor都可以得知房间内发生了什么事情,但又不会造成同时访问怪物A的属性,导致的共享加锁问题

基础用法

Actor并没有作为内置组件,需要先引入包并进行基础配置才能够使用

  1. composer require easyswoole/actor=2.x-dev
建立一个Actor

每一种对象(玩家、房间、甚至是日志服务也可以作为一种Actor对象)都建立一个Actor来进行管理,一个对象可以拥有多个实例(Client)并且可以互相通过信箱发送消息来处理业务

  1. <?php
  2. namespace App\Player;
  3. use EasySwoole\Actor\AbstractActor;
  4. use EasySwoole\Actor\ActorConfig;
  5. /**
  6. * 玩家Actor
  7. * Class PlayerActor
  8. * @package App\Player
  9. */
  10. class PlayerActor extends AbstractActor
  11. {
  12. /**
  13. * 配置当前的Actor
  14. * @param ActorConfig $actorConfig
  15. */
  16. public static function configure(ActorConfig $actorConfig)
  17. {
  18. $actorConfig->setActorName('PlayerActor');
  19. $actorConfig->setWorkerNum(3);
  20. }
  21. /**
  22. * Actor首次启动时
  23. */
  24. protected function onStart()
  25. {
  26. $actorId = $this->actorId();
  27. echo "Player Actor {$actorId} onStart\n";
  28. }
  29. /**
  30. * Actor收到消息时
  31. * @param $msg
  32. */
  33. protected function onMessage($msg)
  34. {
  35. $actorId = $this->actorId();
  36. echo "Player Actor {$actorId} onMessage\n";
  37. }
  38. /**
  39. * Actor即将退出前
  40. * @param $arg
  41. */
  42. protected function onExit($arg)
  43. {
  44. $actorId = $this->actorId();
  45. echo "Player Actor {$actorId} onExit\n";
  46. }
  47. /**
  48. * Actor发生异常时
  49. * @param \Throwable $throwable
  50. */
  51. protected function onException(\Throwable $throwable)
  52. {
  53. $actorId = $this->actorId();
  54. echo "Player Actor {$actorId} onException\n";
  55. }
  56. }
注册Actor服务

可以使用setListenAddress和setListenPort指定本机对外监听的端口,其他机器可以通过该端口向本机的Actor发送消息

  1. public static function mainServerCreate(EventRegister $register) {
  2. // 注册Actor管理器
  3. $server = ServerManager::getInstance()->getSwooleServer();
  4. Actor::getInstance()->register(PlayerActor::class);
  5. Actor::getInstance()->setTempDir(EASYSWOOLE_TEMP_DIR)
  6. ->setListenAddress('0.0.0.0')->setListenPort('9900')->attachServer($server);
  7. }
Actor实例管理

服务启动后就可以进行Actor的操作,管理本机的Client实例,则不需要给client传入$node参数,默认的node为本机,管理其他机器时需要传入

  1. // 管理本机的Actor则不需要声明节点
  2. $node = new ActorNode;
  3. $node->setIp('127.0.0.1');
  4. $node->setListenPort(9900);
  5. // 启动一个Actor并得到ActorId 后续操作需要依赖ActorId
  6. $actorId = PlayerActor::client($node)->create(['time' => time()]); // 00101000000000000000001
  7. // 给某个Actor发消息
  8. PlayerActor::client($node)->send($actorId, ['data' => 'data']);
  9. // 给该类型的全部Actor发消息
  10. PlayerActor::client($node)->sendAll(['data' => 'data']);
  11. // 退出某个Actor
  12. PlayerActor::client($node)->exit($actorId, ['arg' => 'arg']);
  13. // 退出全部Actor
  14. PlayerActor::client($node)->exitAll(['arg' => 'arg']);