异步匹配机制

联机逻辑开发进度:■■■■■□□□□□□□

本章结束开发进度:■■■■■■■□□□□□

上一章的答案

DataCenter类:

  1. <?php
  2. ...
  3. class DataCenter
  4. {
  5. const PREFIX_KEY = "game";
  6. ...
  7. public static function getPlayerWaitListLen()
  8. {
  9. $key = self::PREFIX_KEY . ":player_wait_list";
  10. return self::redis()->lLen($key);
  11. }
  12. public static function pushPlayerToWaitList($playerId)
  13. {
  14. $key = self::PREFIX_KEY . ":player_wait_list";
  15. self::redis()->lPush($key, $playerId);
  16. }
  17. public static function popPlayerFromWaitList()
  18. {
  19. $key = self::PREFIX_KEY . ":player_wait_list";
  20. return self::redis()->rPop($key);
  21. }
  22. public static function getPlayerFd($playerId)
  23. {
  24. $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
  25. return self::redis()->get($key);
  26. }
  27. public static function setPlayerFd($playerId, $playerFd)
  28. {
  29. $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
  30. self::redis()->set($key, $playerFd);
  31. }
  32. public static function delPlayerFd($playerId)
  33. {
  34. $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
  35. self::redis()->del($key);
  36. }
  37. public static function getPlayerId($playerFd)
  38. {
  39. $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
  40. return self::redis()->get($key);
  41. }
  42. public static function setPlayerId($playerFd, $playerId)
  43. {
  44. $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
  45. self::redis()->set($key, $playerId);
  46. }
  47. public static function delPlayerId($playerFd)
  48. {
  49. $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
  50. self::redis()->del($key);
  51. }
  52. public static function setPlayerInfo($playerId, $playerFd)
  53. {
  54. self::setPlayerId($playerFd, $playerId);
  55. self::setPlayerFd($playerId, $playerFd);
  56. }
  57. public static function delPlayerInfo($playerFd)
  58. {
  59. $playerId = self::getPlayerId($playerFd);
  60. self::delPlayerFd($playerId);
  61. self::delPlayerId($playerFd);
  62. }
  63. }

我们先来测试一下前面所写的代码有没有问题,重新运行Server.php,并在浏览器打开游戏前端页面。

查看Redis中的键值:

  1. 127.0.0.1:6379> keys *
  2. 1) "game:player_fd:player_177"
  3. 2) "game:player_id:9"

可以看到,player_idplayer_fd都已经保存下来了。

发送一个匹配请求,并查看Redis中的键值:

  1. 127.0.0.1:6379> keys *
  2. 1) "game:player_fd:player_177"
  3. 2) "game:player_wait_list"
  4. 3) "game:player_id:9"
  5. 127.0.0.1:6379> lrange game:player_wait_list 0 -1
  6. 1) "player_177"

可以看到,匹配队列game:player_wait_list中已经成功存入了player_177

目前我们的匹配机制已经完成了:

  • 前端连接时发送player_id
  • 服务端连接时保存玩家信息
  • 前端发送code600的指令
  • 服务端将player_id放入匹配队列

7 异步匹配机制 - 图1

剩下的操作就是:

  • 检测匹配队列长度,当长度大于等于2时,创建游戏房间并通知客户端

异步检测匹配队列

我们大部分游戏逻辑都是运行在worker里的,异步的玩家匹配可以减轻主进程worker的负担。

Swoole框架里,我们可以使用Task机制,通过投递一个异步任务来避免主进程阻塞。关于Task机制不了解的童鞋,请先熟悉一下官方文档,或者阅读小册的附录二:Swoole入门篇(下)

Swoole Task:wiki.swoole.com/wiki/page/1…

看完文档还是一知半解?那是很正常的,只是缺乏实战经验,下面赵童鞋就带你实战一次。

做题时间

  1. 根据官方文档,在Server类中完成Task机制的初始化。

    <?php … class Server {

    1. ...
    2. const CONFIG = [
    3. ...
    4. 'task_worker_num' => 4,
    5. ...
    6. ];
    7. public function __construct()
    8. {
    9. ...
    10. $this->ws->on('task', [$this, 'onTask']);
    11. $this->ws->on('finish', [$this, 'onFinish']);
    12. ...
    13. }
    14. ...
    15. public function onTask($server, $taskId, $srcWorkerId, $data)
    16. {
    17. }
    18. public function onFinish($server, $taskId, $data)
    19. {
    20. }

    } …

我们什么时候会用Task进行匹配队列检测呢?其实就是把玩家放入匹配队列后。

下面用伪代码进行讲解。

Logic类:

  1. <?php
  2. ...
  3. class Logic
  4. {
  5. public function matchPlayer($playerId)
  6. {
  7. //将用户放入队列中
  8. DataCenter::pushPlayerToWaitList($playerId);
  9. //发起一个Task尝试匹配
  10. //swoole_server->task(xxx);
  11. }
  12. }

Server类:

  1. <?php
  2. ...
  3. class Server
  4. {
  5. ...
  6. public function onTask($server, $taskId, $srcWorkerId, $data)
  7. {
  8. DataCenter::log("onTask", $data);
  9. //执行某些逻辑
  10. }
  11. ...
  12. }
  13. ...

可以发现,onTask()方法只是接收传递的$data,当我们有多种Task任务(匹配玩家、在线检测、游戏状态检查)时,我们的onTask()方法怎么区分每一个Task任务呢?其实就和客户端与服务端通信一样,我们可以根据一个code来区分。

Logic类:

  1. <?php
  2. ...
  3. class Logic
  4. {
  5. public function matchPlayer($playerId)
  6. {
  7. //将用户放入队列中
  8. DataCenter::pushPlayerToWaitList($playerId);
  9. //发起一个Task尝试匹配
  10. //swoole_server->task(['code'=>'xxx']);
  11. }
  12. }

Server类:

  1. <?php
  2. ...
  3. class Server
  4. {
  5. ...
  6. public function onTask($server, $taskId, $srcWorkerId, $data)
  7. {
  8. DataCenter::log("onTask", $data);
  9. switch ($data['code']) {
  10. //执行task方法
  11. case 'xxx':
  12. //task->xxx();
  13. break;
  14. case 'yyy':
  15. //task->yyy();
  16. break;
  17. }
  18. }
  19. ...
  20. }
  21. ...

从代码可以看出,我们现在缺了两种机制:

  • 获取Server对象:在Logic中获取swoole_server对象从而调用task()方法。
  • Task管理类:可以使用一个类来管理TaskcodeTask需要执行的逻辑方法。

全局获取Server对象

这个比较好处理,我们在onWorkerStart的时候就能获取到swoole_server

有童鞋可以会问,为什么不在onStart的时候获取?这是因为onStart回调的是Master进程,而onWorkerStart回调的是Worker进程,只有Worker进程才可以发起Task任务。有兴趣的童鞋请查阅文档:wiki.swoole.com/wiki/page/p…

做题时间

  1. DataCenter中新增静态变量$server
  2. onWorkerStart回调函数中,将$server保存到DataCenter中。

DataCenter类:

  1. <?php
  2. ...
  3. class DataCenter
  4. {
  5. ...
  6. public static $server;
  7. ...
  8. }

Server类:

  1. <?php
  2. ...
  3. class Server
  4. {
  5. ...
  6. public function onWorkerStart($server, $workerId)
  7. {
  8. ...
  9. DataCenter::$server = $server;
  10. }
  11. ...
  12. }
  13. ...

这样就解决了第一种问题,下面轮到第二个问题。

增加Task管理类

在项目Manager文件夹下,创建TaskManager类文件。

TaskManager类:

  1. <?php
  2. namespace App\Manager;
  3. class TaskManager
  4. {
  5. }

后续所有跟task有关的常量、方法都归于这个类来管理。

做题时间

  1. 设置一个常量TASK_CODE_FIND_PLAYER,用于发起寻找玩家task任务。
  2. 新增静态方法findPlayer(),当匹配队列长度大于等于2时,弹出队列前两个玩家的player_id并返回。

TaskManager类:

  1. <?php
  2. namespace App\Manager;
  3. class TaskManager
  4. {
  5. const TASK_CODE_FIND_PLAYER = 1;
  6. public static function findPlayer()
  7. {
  8. $playerListLen = DataCenter::getPlayerWaitListLen();
  9. if ($playerListLen >= 2) {
  10. $redPlayer = DataCenter::popPlayerFromWaitList();
  11. $bluePlayer = DataCenter::popPlayerFromWaitList();
  12. return [
  13. 'red_player' => $redPlayer,
  14. 'blue_player' => $bluePlayer
  15. ];
  16. }
  17. return false;
  18. }
  19. }

现在前置准备就绪,可以将上面写过的伪代码改成真实代码啦~

做题时间

  1. Logic类的matchPlayer()方法中,发起一个Task任务尝试匹配。
  2. Server类的onTask()方法中根据传入的code,执行TaskManagerfindPlayer()方法。
  3. findPlayer()方法有返回值的时候,返回执行结果并携带上codeworker进程的onFinish()方法中。

本章就到这里结束了,这次留的Homework可能有点难度,请童鞋们尽力完成。

本章对应Github Commit第七章结束

当前目录结构:

  1. HideAndSeek
  2. ├── app
  3. ├── Lib
  4. └── Redis.php
  5. ├── Manager
  6. ├── DataCenter.php
  7. ├── Game.php
  8. ├── Logic.php
  9. └── TaskManager.php
  10. ├── Model
  11. ├── Map.php
  12. └── Player.php
  13. └── Server.php
  14. ├── composer.json
  15. ├── composer.lock
  16. ├── frontend
  17. └── index.html
  18. ├── test.php
  19. └── vendor
  20. ├── autoload.php
  21. └── composer