Swoole和WebSocket之斗地主上篇进房间
这篇文章主要讲解通过Swoole
和 webSocket
来实现斗地主的用户进入房间功能,这也是实现斗地主的第一步。实现整个斗地主分为三篇文章来讲解这是第一篇。
在技术栈方面主要使用php
, swoole, html5:webSocket
, pixi.js, 数据存储采用redis
。 主要实现效果如下图:
开始
在开始之前我们默认已有php
开始环境。我们只需要安装swoole
扩展,redis
扩展和redis
包。
-
安装
swoole
,redis
. -
下载
pixijs
库, 当然你可以使用远程的. (一个绘制2D图形的引擎) -
composer.json
编写, 之后执行安装命令即可composer install
.
{
"require": {
"meshell/surf": "^1.0.5" //对swoole一个封装框架(https://github.com/TianLiangZhou/surf)
},
"autoload": {
"psr-4": {
"Landowner\\": "./app" //后台服务的文件目录
}
}
}
- 建立入口(
index.html
)文件,资源目录,服务目录和文件。如图:
功能实现
我们需要实现websocket
和后台服务的对接。制定协议格式,比如协议名,内容,返回值。
进入房间
一个斗地主流程比较完善应该是:
用户登录 -> 选择房间 -> 进入房间 -> 准备开始 -> 开始.
我们在这里的实现直接从进入房间开始。用户直接打开网页就是进入房间。
- 功能分析
当前用户进入房间之前,我们需要在页面显示当前房间有多少人等待,进入成功之后我们需要告知其它用户有人进来了。从上面得知我们需要一个获取当前房间有多少人的协议,
这些人的状态是什么(准备中
或未准备
),还需要一个进入房间的协议,该协议要实现广播告知其它用户,而客户端也需要监听进入房间的广播消息。
- 服务端
我们先从服务端代码开始编写. 根据上面的分析我需要实现两个协议. surf
也是一个MVC
形式的框架. 我们只需要实现自己的业务控制器就行了.
在这里我们使用框架自带的json
数据传输类型来解析.
- 入口文件
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) {
}
- 协议路由
protocol.php
<?php
use Landowner\Protocol\LandownerController;
$app->addProtocol( //进入房间协议
'enter.room', //协议名称
LandownerController::class . ':enterRoom'
);
$app->addProtocol( //获取当前房间人数列表协议
'room.player',
LandownerController::class . ':roomPlayer'
);
- 控制器的实现
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