异步匹配机制
联机逻辑开发进度:■■■■■□□□□□□□
本章结束开发进度:■■■■■■■□□□□□
上一章的答案
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 -1
1) "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
类:
<?php
namespace App\Manager;
class TaskManager
{
}
后续所有跟task
有关的常量、方法都归于这个类来管理。
做题时间
- 设置一个常量
TASK_CODE_FIND_PLAYER
,用于发起寻找玩家task
任务。 - 新增静态方法
findPlayer()
,当匹配队列长度大于等于2
时,弹出队列前两个玩家的player_id
并返回。
TaskManager
类:
<?php
namespace 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