异步匹配机制
联机逻辑开发进度:■■■■■□□□□□□□
本章结束开发进度:■■■■■■■□□□□□
上一章的答案
DataCenter类:
<?php...class DataCenter{const PREFIX_KEY = "game";...public static function getPlayerWaitListLen(){$key = self::PREFIX_KEY . ":player_wait_list";return self::redis()->lLen($key);}public static function pushPlayerToWaitList($playerId){$key = self::PREFIX_KEY . ":player_wait_list";self::redis()->lPush($key, $playerId);}public static function popPlayerFromWaitList(){$key = self::PREFIX_KEY . ":player_wait_list";return self::redis()->rPop($key);}public static function getPlayerFd($playerId){$key = self::PREFIX_KEY . ":player_fd:" . $playerId;return self::redis()->get($key);}public static function setPlayerFd($playerId, $playerFd){$key = self::PREFIX_KEY . ":player_fd:" . $playerId;self::redis()->set($key, $playerFd);}public static function delPlayerFd($playerId){$key = self::PREFIX_KEY . ":player_fd:" . $playerId;self::redis()->del($key);}public static function getPlayerId($playerFd){$key = self::PREFIX_KEY . ":player_id:" . $playerFd;return self::redis()->get($key);}public static function setPlayerId($playerFd, $playerId){$key = self::PREFIX_KEY . ":player_id:" . $playerFd;self::redis()->set($key, $playerId);}public static function delPlayerId($playerFd){$key = self::PREFIX_KEY . ":player_id:" . $playerFd;self::redis()->del($key);}public static function setPlayerInfo($playerId, $playerFd){self::setPlayerId($playerFd, $playerId);self::setPlayerFd($playerId, $playerFd);}public static function delPlayerInfo($playerFd){$playerId = self::getPlayerId($playerFd);self::delPlayerFd($playerId);self::delPlayerId($playerFd);}}
我们先来测试一下前面所写的代码有没有问题,重新运行Server.php,并在浏览器打开游戏前端页面。
查看Redis中的键值:
127.0.0.1:6379> keys *1) "game:player_fd:player_177"2) "game:player_id:9"
可以看到,player_id和player_fd都已经保存下来了。
发送一个匹配请求,并查看Redis中的键值:
127.0.0.1:6379> keys *1) "game:player_fd:player_177"2) "game:player_wait_list"3) "game:player_id:9"127.0.0.1:6379> lrange game:player_wait_list 0 -11) "player_177"
可以看到,匹配队列game:player_wait_list中已经成功存入了player_177。
目前我们的匹配机制已经完成了:
- 前端连接时发送
player_id - 服务端连接时保存玩家信息
- 前端发送
code为600的指令 - 服务端将
player_id放入匹配队列

剩下的操作就是:
- 检测匹配队列长度,当长度大于等于2时,创建游戏房间并通知客户端
异步检测匹配队列
我们大部分游戏逻辑都是运行在worker里的,异步的玩家匹配可以减轻主进程worker的负担。
在Swoole框架里,我们可以使用Task机制,通过投递一个异步任务来避免主进程阻塞。关于Task机制不了解的童鞋,请先熟悉一下官方文档,或者阅读小册的附录二:Swoole入门篇(下)
Swoole Task:wiki.swoole.com/wiki/page/1…
看完文档还是一知半解?那是很正常的,只是缺乏实战经验,下面赵童鞋就带你实战一次。
做题时间
根据官方文档,在
Server类中完成Task机制的初始化。<?php … class Server {
...const CONFIG = [...'task_worker_num' => 4,...];public function __construct(){...$this->ws->on('task', [$this, 'onTask']);$this->ws->on('finish', [$this, 'onFinish']);...}...public function onTask($server, $taskId, $srcWorkerId, $data){}public function onFinish($server, $taskId, $data){}
} …
我们什么时候会用Task进行匹配队列检测呢?其实就是把玩家放入匹配队列后。
下面用伪代码进行讲解。
Logic类:
<?php...class Logic{public function matchPlayer($playerId){//将用户放入队列中DataCenter::pushPlayerToWaitList($playerId);//发起一个Task尝试匹配//swoole_server->task(xxx);}}
Server类:
<?php...class Server{...public function onTask($server, $taskId, $srcWorkerId, $data){DataCenter::log("onTask", $data);//执行某些逻辑}...}...
可以发现,onTask()方法只是接收传递的$data,当我们有多种Task任务(匹配玩家、在线检测、游戏状态检查)时,我们的onTask()方法怎么区分每一个Task任务呢?其实就和客户端与服务端通信一样,我们可以根据一个code来区分。
Logic类:
<?php...class Logic{public function matchPlayer($playerId){//将用户放入队列中DataCenter::pushPlayerToWaitList($playerId);//发起一个Task尝试匹配//swoole_server->task(['code'=>'xxx']);}}
Server类:
<?php...class Server{...public function onTask($server, $taskId, $srcWorkerId, $data){DataCenter::log("onTask", $data);switch ($data['code']) {//执行task方法case 'xxx'://task->xxx();break;case 'yyy'://task->yyy();break;}}...}...
从代码可以看出,我们现在缺了两种机制:
- 获取Server对象:在
Logic中获取swoole_server对象从而调用task()方法。 - Task管理类:可以使用一个类来管理
Task的code和Task需要执行的逻辑方法。
全局获取Server对象
这个比较好处理,我们在onWorkerStart的时候就能获取到swoole_server。
有童鞋可以会问,为什么不在
onStart的时候获取?这是因为onStart回调的是Master进程,而onWorkerStart回调的是Worker进程,只有Worker进程才可以发起Task任务。有兴趣的童鞋请查阅文档:wiki.swoole.com/wiki/page/p…做题时间
- 在
DataCenter中新增静态变量$server。 - 在
onWorkerStart回调函数中,将$server保存到DataCenter中。
DataCenter类:
<?php...class DataCenter{...public static $server;...}
Server类:
<?php...class Server{...public function onWorkerStart($server, $workerId){...DataCenter::$server = $server;}...}...
这样就解决了第一种问题,下面轮到第二个问题。
增加Task管理类
在项目Manager文件夹下,创建TaskManager类文件。
TaskManager类:
<?phpnamespace App\Manager;class TaskManager{}
后续所有跟task有关的常量、方法都归于这个类来管理。
做题时间
- 设置一个常量
TASK_CODE_FIND_PLAYER,用于发起寻找玩家task任务。 - 新增静态方法
findPlayer(),当匹配队列长度大于等于2时,弹出队列前两个玩家的player_id并返回。
TaskManager类:
<?phpnamespace App\Manager;class TaskManager{const TASK_CODE_FIND_PLAYER = 1;public static function findPlayer(){$playerListLen = DataCenter::getPlayerWaitListLen();if ($playerListLen >= 2) {$redPlayer = DataCenter::popPlayerFromWaitList();$bluePlayer = DataCenter::popPlayerFromWaitList();return ['red_player' => $redPlayer,'blue_player' => $bluePlayer];}return false;}}
现在前置准备就绪,可以将上面写过的伪代码改成真实代码啦~
做题时间
- 在
Logic类的matchPlayer()方法中,发起一个Task任务尝试匹配。 - 在
Server类的onTask()方法中根据传入的code,执行TaskManager的findPlayer()方法。 - 当
findPlayer()方法有返回值的时候,返回执行结果并携带上code到worker进程的onFinish()方法中。
本章就到这里结束了,这次留的Homework可能有点难度,请童鞋们尽力完成。
本章对应Github Commit:第七章结束
当前目录结构:
HideAndSeek├── app│ ├── Lib│ │ └── Redis.php│ ├── Manager│ │ ├── DataCenter.php│ │ ├── Game.php│ │ ├── Logic.php│ │ └── TaskManager.php│ ├── Model│ │ ├── Map.php│ │ └── Player.php│ └── Server.php├── composer.json├── composer.lock├── frontend│ └── index.html├── test.php└── vendor├── autoload.php└── composer
