Swoole和WebSocket之斗地主上篇进房间

2,614次阅读

共计 8491 个字符,预计需要花费 22 分钟才能阅读完成。

Swoole 和 WebSocket 之斗地主上篇进房间

这篇文章主要讲解通过 Swoole  和  webSocket  来实现斗地主的用户进入房间功能,这也是实现斗地主的第一步。实现整个斗地主分为三篇文章来讲解这是第一篇。 
在技术栈方面主要使用phpswoolehtml5:webSocketpixi.js, 数据存储采用redis。主要实现效果如下图:

开始

在开始之前我们默认已有 php 开始环境。我们只需要安装 swoole 扩展,redis扩展 redis

  1. 安装swooleredis.
  2. 下载 pixijs 库,当然你可以使用远程的. (一个绘制 2D 图形的引擎)
  3. composer.json  编写, 之后执行安装命令即可composer install.
{
    "require": {"meshell/surf": "^1.0.5" // 对 swoole 一个封装框架(https://github.com/TianLiangZhou/surf)
    },
    "autoload": {
        "psr-4": {"Landowner\\": "./app" // 后台服务的文件目录}
    }
}
  1. 建立入口 (index.html) 文件,资源目录,服务目录和文件。如图:
Swoole 和 WebSocket 之斗地主上篇进房间

功能实现

我们需要实现 websocket 和后台服务的对接。制定协议格式,比如协议名,内容,返回值。

进入房间

一个斗地主流程比较完善应该是:

用户登录 -> 选择房间 -> 进入房间 -> 准备开始 -> 开始.

我们在这里的实现直接从进入房间开始。用户直接打开网页就是进入房间。

  • 功能分析

当前用户进入房间之前,我们需要在页面显示当前房间有多少人等待,进入成功之后我们需要告知其它用户有人进来了。从上面得知我们需要一个获取当前房间有多少人的协议, 
这些人的状态是什么(准备中 未准备), 还需要一个进入房间的协议,该协议要实现广播告知其它用户,而客户端也需要监听进入房间的广播消息。

  • 服务端

我们先从服务端代码开始编写. 根据上面的分析我需要实现两个协议. surf也是一个 MVC 形式的框架. 我们只需要实现自己的业务控制器就行了. 
在这里我们使用框架自带的 json 数据传输类型来解析.

  1. 入口文件landowner.php
php

require __DIR__ . '/vendor/autoload.php';
$config = [];
$config['setting'] = ['document_root' => __DIR__,
    'task_worker_num' => 1,
    ];
$config['server'] = 'webSocket';
$app = new \Surf\Application(__DIR__, ['app.config' => $config
]);
$app->register(new \Surf\Provider\RedisServiceProvider());
include __DIR__ . '/protocol.php'; // 协议路由文件
try {$app->run();} catch (\Surf\Exception\ServerNotFoundException $e) {}
  1. 协议路由protocol.php
   <?php 
   use Landowner\Protocol\LandownerController;
   $app->addProtocol( // 进入房间协议
       \'enter.room\', // 协议名称
       LandownerController::class . \':enterRoom\'
   );

   $app->addProtocol( // 获取当前房间人数列表协议
       \'room.player\',
       LandownerController::class . \':roomPlayer\'
   );
  1. 控制器的实现LandownerController.php

    <?php

    namespace Landowner\Protocol;

    use Pimple\Psr11\Container;
    use Surf\Mvc\Controller\WebSocketController;
    use Surf\Server\RedisConstant;
    use Surf\Task\PushTaskHandle;

    class LandownerController extends WebSocketController
    {
        const READY_KEY = \'ready:action\';

        /**
         * @var null | \Redis
         */
        protected $redis = null;

        /**
         * LandownerController constructor.
         * @param Container $container
         * @param int $workerId
         */
        public function __construct(Container $container, $workerId = 0)
        {parent::__construct($container, $workerId);

            $this->redis = $this->container->get(\'redis\');
        }


        /**
         * @param $body
         * @return array
         */
        public function enterRoom($body)
        {
            // 框架自带类常量,获取当前的总数
            $count = $this->redis->sCard(RedisConstant::FULL_CONNECT_FD);
            $flag = 0;
            $players = [];
            if ($count> 3) { // 限制 3
                $flag = 500;
                $this->setIsClose(true); // 主动断开
            } else {$allPlayer = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);
                print_r($allPlayer);
                $otherPlayer = array_diff($allPlayer, [$this->frame->fd]);
                if ($otherPlayer) { // 找出其它用户
                    foreach ($otherPlayer as $fd) {$readyState = $this->redis->hGet(self::READY_KEY, $fd);
                        $players[] = [\'ready\' => $readyState, // 获取其它用户的状态
                            \'playerId\' => $fd, // 用户 id
                        ];
                    }
                    $this->task(["from" => $otherPlayer,
                        "content" => json_encode(["listen" => "enterRoom", // 客户监听的协议名称
                            "content" => $this->frame->fd,
                        ])
                    ], PushTaskHandle::class); // 通过任务给其它用户发送消息
                }
            }
            return ["flag" => $flag,
                "player" => $this->frame->fd,
                "otherPlayer" => $players,
                "requestId" => $body->requestId,
            ];
        }

        /**
         * 获取正在房间的用户,以及状态,客户端要据状态显示不同的文字 
         * 
         * @param $body
         * @return array
         */
        public function roomPlayer($body)
        {$count = $this->redis->sCard(RedisConstant::FULL_CONNECT_FD);
            $player = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);
            $players = [];
            foreach ($player as $fd) {if ($fd == $this->frame->fd) {continue;}
                $readyState = $this->redis->hGet(self::READY_KEY, $fd);
                $players[] =[\'ready\' => $readyState,
                    \'playerId\'=> $fd,
                ];
            }
            return ["flag"   => 0,
                "count" => $count,
                "player" => $players,
                "requestId" => $body->requestId,
            ];
        }
       ...
    }
  • 客户端

在客户端我们这里使用原生的 webSocket 进行通信,pixijs引擎进行渲染界面. 在这里封装了一个简单的 websocket 使用类. 整个 socket 类代码.


function Socket() {
    var isConnection = false, _this = this;
    var connection = function() {
        var host = location.host;
        if (host !== "example.loocode.com") {host = "192.168.56.101";}
        return new WebSocket("ws://"   host   ":9527");
    };

    this.reconnect = function() {this.socket = connection();
        this.socket.onopen = function () {
            isConnection = true;
            if (_this.openCallback) {_this.openCallback.call(_this);
            }
        };
        this.socket.onmessage = function(event) {_this.response(event); // 监听消息
        };
        this.socket.onclose = function() {isConnection = false;};
        this.socket.onerror = function(e) {alert("connection websocket error!");
        };
    };
    this.response = function(event) {
        var response = null;
        try {response = JSON.parse(event.data);
        } catch (e) {console.log(e);
        }
        console.log(response);
        // 协议返回
        if (typeof response.body === "object" && response.body !== undefined) {
            var requestId = response.body.requestId; 
            var callback = this.getRequestCallback(requestId);
            if (callback !== undefined) {callback.call(_this, response.body);
            }
        }
        // 服务主动发送的监听
        if (typeof response.listen === "string" && response.listen !== undefined) {if (_this.listeners.hasOwnProperty(response.listen)) {_this.listeners[response.listen].call(_this, response.content);
            }
        }
    };
    this.isConnected = function() {return isConnection;};
    this.requestCallback = {};
    this.listeners = {};
    this.openCallback = null;
}

Socket.prototype = {addRequestCallback: function (id, callback) {this.requestCallback[id] = callback;
    },
    getRequestCallback: function(id) {if (this.requestCallback.hasOwnProperty(id)) {return this.requestCallback[id];
        }
        return undefined;
    },
    listen: function (name, callback) {this.listeners[name] = callback;
    },
    onOpenCallback: function(callback) {if (callback !== undefined && callback !== null) {this.openCallback = callback;}
    },
    send: function(protocol, body, callback) {var requestId = new Date().getTime();
        if (body === undefined || body === null) {body = {requestId: requestId};
        } else {body.requestId = requestId;}
        var requestBody = {
            "protocol": protocol,
            "body": body
        };
        if (!this.isConnected()) {this.reconnect();
        }
        if (callback !== undefined) {this.addRequestCallback(requestId, callback);
        }
        this.socket.send(JSON.stringify(requestBody));
    }
};

实现的客户业务代码


function Landowner() {
    this.app = new PIXI.Application( // 初始化容器
        window.innerWidth,
        window.innerHeight,
        {backgroundColor: 0x1099bb, // 设置容器颜色}
    );
    this.socket = new Socket();
    this.playerCount = 0; // 用户总数
    this.player = 0; // 用户 id
    this.playerReadyButton = {};
    this.playId = 0; // 当前牌局 id
}

Landowner.prototype = {start: function () {document.body.appendChild(this.app.view);
        this.listen(); // 初始化监听},
    initRender: function (landowner) {
        /**
         * @var landowner Landowner
         */
        if (this.socket.isConnected()) {this.socket.send(\'room.player\', {}, function(body) {if (body.count> 3) {return ;}
                landowner.playerCount = body.count;
                for (var i = 0; i < body.player.length; i) {landowner.readyWorker(i   2, !!body.player[i].ready, body.player[i].playerId);
                }
                this.send("enter.room", {}, function (body) {if (body.flag === 0) {
                        landowner.player = body.player;
                        landowner.readyWorker(1, false, body.player);
                    }
                });
            });
        }
    },
    listen: function() {
        var _this = this;
        this.socket.listen("enterRoom", function(content) {
            _this.playerCount  ;
            _this.readyWorker(_this.playerCount, false, content);
        });
        this.socket.listen("readyStatus", function(content) {
            /**
             * @var button Graphics
             */
            var button = _this.playerReadyButton[content.playerId];
            var text = button.getChildByName(\'text\');
            text.text = content.ready ? "准备中" : "准备";
        });
        this.socket.listen("assignPoker", function(content) {
            _this.playId = content.playId;
            _this.renderBottomCard(content.landowner);
            _this.assignPoker(content.poker);
            for (var key in _this.playerReadyButton) {_this.playerReadyButton[key].destroy();}
        });
        this.socket.onOpenCallback(function() {_this.initRender(_this);
        });
        this.socket.reconnect();},
    readyWorker: function (offset, state, playId) {
        var _this = this;
        var button = new PIXI.Graphics()
            .beginFill(0x2fb44a)
            .drawRoundedRect(0, 0, 120, 60, 10)
            .endFill();
        var text = "准备";
        if (state === true) {text = "准备中";}
        var readyText = new PIXI.Text(text, new PIXI.TextStyle({
            fontFamily: "Arial",
            fontSize: 32,
            fill: "white",
        }));
        readyText.x = 60 - 32;
        readyText.y = 30 - 16;
        readyText.name = "text";
        button.addChild(readyText);
        button.interactive = true;
        button.buttonMode = true;
        var clickCounter = 1;
        if (offset === 1) {button.on(\'pointertap\', function () {
                var ready = 0;
                if (clickCounter % 2) {
                    readyText.text = "取消";
                    ready = 1;
                } else {readyText.text = "准备";}
                clickCounter  ;
                _this.socket.send(\'player.ready\', {\'ready\': ready}, function (body) {console.log(body);
                });
            });
        }
        var x = y = 0;
        y = window.innerHeight / 2;
        x = window.innerWidth / 2;
        if (offset === 2) {x = x   (x / 2);
            y = y - (y / 2);
        } else if (offset === 3) {x = x - (x / 2) - 60;
            y = y - (y / 2);
        } else {
            x = x - 60;
            y = y   (y / 2);
        }
        button.x = x;
        button.y = y;
        this.playerReadyButton[playId] = button;
        this.app.stage.addChild(button);
    },
}

入口文件index.html

<!DOCTYPE html> 
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Landowner</title>
    <script src="js/pixi.js"></script>
</head>
<body>
    <script type="text/javascript" src="js/landowner.js"></script>
    <script type="text/javascript">
        new Landowner().start();
    </script>
</body>
</html>

以上服务端和客户端代码就完成了用户进入房间功能。下一期我们讲下准备,发牌的实现.

源码地址

https://github.com/TianLiangZhou/loocode-example/tree/master/landowner

效果地址

https://example.loocode.com/landowner/index.html

推荐阅读

  1. https://wiki.swoole.com/
  2. https://github.com/TianLiangZhou/surf
  3. https://pixijs.io/examples/
  4. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

正文完
 0
Blood.Cold
版权声明:本站原创文章,由 Blood.Cold 于2019-06-04发表,共计8491字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。