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

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

上一章的答案:

Logic类:

<?php...class Logic{    public function matchPlayer($playerId)    {        ...        //发起一个Task尝试匹配        DataCenter::$server->task(['code' => TaskManager::TASK_CODE_FIND_PLAYER]);    }}

Server类:

<?php...class Server{    ...    public function onTask($server, $taskId, $srcWorkerId, $data)    {        DataCenter::log("onTask", $data);        $result = [];        switch ($data['code']) {            case TaskManager::TASK_CODE_FIND_PLAYER:                $ret = TaskManager::findPlayer();                if (!empty($ret)) {                    $result['data'] = $ret;                }                break;        }        if (!empty($result)) {            $result['code'] = $data['code'];            return $result;        }    }    ...}...

童鞋们的作业完成情况如何呢?

我们来再次梳理一下目前的匹配功能进度:

  • 前端连接时发送player_id
  • 服务端连接时保存玩家信息
  • 前端发送code600的指令
  • 服务端将player_id放入匹配队列
  • 服务端发起一个task进行玩家匹配,当寻找到两个玩家时返回两个player_idworker进程

那下一步就很明显了,就是创建游戏房间。

创建房间分析

  1. Server类的onFinish()方法中,根据传入的code,执行LogiccreateRoom()方法。

Server类:

<?php...class Server{    ...    public function onFinish($server, $taskId, $data)    {        DataCenter::log("onFinish", $data);        switch ($data['code']) {            case TaskManager::TASK_CODE_FIND_PLAYER:                $this->logic->createRoom($data['data']['red_player'],                                          $data['data']['blue_player']);                break;        }    }}...

显然,下一步就是完成这个createRoom()方法匹配机制就大功告成了。但是真的这么简单吗?下面我们要思考一件事情。

我们的匹配队列是存放在Redis中的,无论哪个worker都可以读取,但游戏数据是存放在内存中的,在启动Swoole Worker时设置了'worker_num' => 4worker是多进程的,这会产生什么效果呢?就是进程内存隔离。

  • Swoole文档:https://wiki.swoole.com/wiki/...

比如,A玩家进入了worker_1,数据保存在worker_1进程内存中,而B玩家进入了worker_2,数据保存在worker_2进程内存中。他们的匹配队列用的却是同一个Redis List,假如我们选择了worker_1进行游戏数据存放,那么B玩家将会读取不到内存中的游戏数据。

要解决这个问题有几个容易的方法:

  • A:说个啥子废话直接把多进程改成单进程就好啦!
  • B:数据不放内存不就得了嘛?找个Redis爱咋放咋放。
  • C:将两个玩家绑定到同一个worker中。

显然,A方法过于粗暴,没想到竟说出如此粗鄙之语!而B方法扩展性不好,当有成千上万玩家的时候,我们的Redis分分钟就挂给你看。这样下来只能选择C方法来实践。

Swoole为我们提供了一个bind()方法,就可将连接绑定到固定的一个worker来处理。不了解bind()方法的童鞋请先阅读一下官方文档,尤其是时序问题

  • Swoole文档:https://wiki.swoole.com/wiki/...

那么我们创建房间的流程就是:

  • 生成一个房间room_id
  • task寻找到的两位玩家连接的fd绑定到room_id算出的同一个int
  • 通知玩家room_id
  • 前端获取到room_id后,发起开始游戏请求

绑定玩家连接

  1. 想要使用bind()方法,需先将dispatch_mode设置为5
  2. 完成LogiccreateRoom()方法,生成一个room_id,绑定连接fd
  3. 获取$server对象,向两个玩家分别发送房间room_id

Server类:

<?php...class Server{    ...    const CONFIG = [        ...        'dispatch_mode' => 5,        ...    ];    ...}...

Logic类:

<?php...class Logic{    ...    public function createRoom($redPlayer, $bluePlayer)    {        $roomId = uniqid('room_');        $this->bindRoomWorker($redPlayer, $roomId);        $this->bindRoomWorker($bluePlayer, $roomId);    }    private function bindRoomWorker($playerId, $roomId)    {        $playerFd = DataCenter::getPlayerFd($playerId);        DataCenter::$server->bind($playerFd, crc32($roomId));        DataCenter::$server->push($playerFd, $roomId);    }}

童鞋们发现问题了吗?

没错,我们的push()方法直接就把room_id发过去了。又是这种问题:接收方无法识别该消息是何种消息。那么我们要如何处理呢?还是老套路,加code协议码。一个更好的办法是,找一个类来专门管理发送相关的变量和方法。

Manager文件夹下,新建Sender类文件。

Sender类:

<?phpnamespace App\Manager;class Sender{}
  1. Sender类中新增MSG_ROOM_ID常量,作为发送room_idcode
  2. 新增方法sendMessage($playerId, $code, $data = []),通过传入的$playerId发送固定格式的消息到客户端。比较常规的内容需要有:codemsgdata
  3. bindRoomWorker()中发送房间room_id的代码改为使用Sender发送。

Sender类:

<?php...class Sender{    const MSG_ROOM_ID = 1001;    const CODE_MSG = [        self::MSG_ROOM_ID => '房间ID',    ];    public static function sendMessage($playerId, $code, $data = [])    {        $message = [            'code' => $code,            'msg' => self::CODE_MSG[$code] ?? '',            'data' => $data        ];        $playerFd = DataCenter::getPlayerFd($playerId);        if (empty($playerFd)) {            return;        }        DataCenter::$server->push($playerFd, json_encode($message));    }}

Logic类:

<?php...class Logic{    ...    private function bindRoomWorker($playerId, $roomId)    {        $playerFd = DataCenter::getPlayerFd($playerId);        DataCenter::$server->bind($playerFd, crc32($roomId));        Sender::sendMessage($playerId, Sender::MSG_ROOM_ID, ['room_id' => $roomId]);    }}

这下我们的前端就能通过接收的code来判断,究竟这条message房间ID或者是游戏数据

我们来测试一下目前为止的代码有没有问题。重启Server服务器,在浏览器打开两个游戏前端页面并点击匹配按钮。

[root@localhost app]# php Server.php master start (listening on 0.0.0.0:8811)server: onWorkStart,worker_id:4server: onWorkStart,worker_id:5server: onWorkStart,worker_id:6server: onWorkStart,worker_id:7server: onWorkStart,worker_id:0server: onWorkStart,worker_id:1server: onWorkStart,worker_id:2server: onWorkStart,worker_id:3[2019-04-21 15:59:46][INFO]: client open fd:3[2019-04-21 15:59:50][INFO]: client open fd:3,message:{"code":600}[2019-04-21 15:59:50][INFO]: onTask {"code":1}[2019-04-21 15:59:50][INFO]: onFinish {"data":{"red_player":"player_177","blue_player":"player_181"},"code":1}PHP Warning:  Swoole\WebSocket\Server::push(): the connected client of connection[9] is not a websocket client or closed. in /mnt/htdocs/HideAndSeek_teach/app/Manager/Sender.php on line 31

显然,程序报错了。这是因为我们启动服务器时,没有清除之前的残余玩家信息,所以push()时报错了。

初始化玩家数据

  1. DataCenter中新增initDataCenter()方法清除Redis中的残余数据。
  2. onStart的时候调用initDataCenter()方法。

DataCenter类:

<?php...class DataCenter{    ...    public static function initDataCenter()    {        //清空匹配队列        $key = self::PREFIX_KEY . ':player_wait_list';        self::redis()->del($key);        //清空玩家ID        $key = self::PREFIX_KEY . ':player_id*';        $values = self::redis()->keys($key);        foreach ($values as $value) {            self::redis()->del($value);        }        //清空玩家FD        $key = self::PREFIX_KEY . ':player_fd*';        $values = self::redis()->keys($key);        foreach ($values as $value) {            self::redis()->del($value);        }    }    ...}

Server类:

<?php...class Server{    ...    public function onStart($server)    {        ...        DataCenter::initDataCenter();    }    ...}...

现在再来一次,重启Server服务器,在浏览器打开两个游戏前端页面并点击匹配按钮。

可以看到,服务端成功发送room_id

发送开始游戏指令

  1. Vue的数据属性中新增roomId,用于保存服务端发送的room_id
  2. 新增方法startRoom(),当服务端发来room_id消息时,发送code以及room_id到服务端开始游戏。

本章留的Homework是前端功能,但是比较简单,请童鞋们尽力完成哦。

当前目录结构:

HideAndSeek├── app│   ├── Lib│   │   └── Redis.php│   ├── Manager│   │   ├── DataCenter.php│   │   ├── Game.php│   │   ├── Logic.php│   │   ├── Sender.php│   │   └── TaskManager.php│   ├── Model│   │   ├── Map.php│   │   └── Player.php│   └── Server.php├── composer.json├── composer.lock├── frontend│   └── index.html├── test.php└── vendor    ├── autoload.php    └── composer