首页 php

easyswoole实现websocket

发布于: 2021-06-18

easyswoole实现websocket的步骤解析

配置 Websocket服务

1
2
3
4
5
6
7
'MAIN_SERVER' => [
'SERVER_TYPE' => EASYSWOOLE_WEB_SOCKET_SERVER,
'SETTING' => [
// 该参数项为心跳检测,严格参考swoole 配置说明
'heartbeat_check_interval' => 60,
],
],

fd与uuid绑定类App/Support/FdManager.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
namespace App\Support;
use EasySwoole\Component\Singleton;
use Swoole\Table;
/**
* Class FdManager
* @package App\Support
*/
class FdManager
{
use Singleton;

/**
* @var Table
*/
private $fdUuid;
/**
* @var Table
*/
private $uuidFd;

/**
* FdManager constructor.
* @param int $size
*/
public function __construct(int $size = 1024 * 256)
{
$this->fdUuid = new Table($size);
$this->fdUuid->column('uuid', Table::TYPE_STRING, 25);
$this->fdUuid->create();
$this->uuidFd = new Table($size);
$this->uuidFd->column('fd', Table::TYPE_INT, 10);
$this->uuidFd->create();
}

/**
* fd 绑定
* @param int $fd
* @param string $uuid
* @return mixed
*/
public function bind(int $fd, $uuid)
{
// TODO: Implement bind() method.
$this->fdUuid->set($fd, ['uuid' => $uuid]);
$this->uuidFd->set($uuid, ['fd' => $fd]);
}

/**
* 删除fd绑定关系
* @param int $fd
* @return mixed
*/
public function delete(int $fd)
{
// TODO: Implement delete() method.
$uuid = $this->fdUuid($fd);
if ($uuid) {
$this->uuidFd->del($uuid);
}
$this->fdUuid->del($fd);
}

/**
* 通过fd找到绑定的uuid
* @param int $fd
* @return mixed
*/
public function fdUuid(int $fd)
{
// TODO: Implement fdUuid() method.
$ret = $this->fdUuid->get($fd);
if ($ret) {
return $ret['uuid'];
} else {
return null;
}
}

/**
* 通过uuid找到fd
* @param $uuid
* @return mixed
*/
public function uuidFd($uuid)
{
// TODO: Implement uuidFd() method.
$ret = $this->uuidFd->get($uuid);
if ($ret) {
return $ret['fd'];
} else {
return null;
}
}
}

webscoekt 事件类/App/WebSocket/WebSocketEvent.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?php
namespace App\WebSocket;
use App\Support\FdManager;
/**
* Class WebSocketEvent
* @package App\WebSocket
*/
class WebSocketEvent
{
/**
* 握手事件
* 所有客户端建立连接时触发的方法
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
public function onHandShake(\swoole_http_request $request, \swoole_http_response $response)
{
/** 此处自定义握手规则 返回 false 时中止握手 */
if (!$this->customHandShake($request, $response)) {
$response->end();
return false;
}
/** 此处是 RFC规范中的WebSocket握手验证过程 必须执行 否则无法正确握手 */
if ($this->secWebsocketAccept($request, $response)) {
$response->end();
return true;
}
$response->end();
return false;
}

/**
* 关闭事件
* 所有客户端关闭时触发的方法
* @param \swoole_server $server
* @param int $fd
* @param int $reactorId
*/
public function onClose(\swoole_server $server, int $fd, int $reactorId)
{
/** @var array $info */
$info = $server->getClientInfo($fd);
/**
* 判断此fd 是否是一个有效的 websocket 连接
* 参见 https://wiki.swoole.com/wiki/page/490.html
*/
if ($info && $info['websocket_status'] === WEBSOCKET_STATUS_FRAME) {
/**
* 判断连接是否是 server 主动关闭
* 参见 https://wiki.swoole.com/wiki/page/p-event/onClose.html
*/
if ($reactorId < 0) {
}
//删除绑定关系
FdManager::getInstance()->delete($fd);
}
}

/**
* RFC规范中的WebSocket握手验证过程
* 以下内容必须强制使用
*
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
protected function secWebsocketAccept(\swoole_http_request $request, \swoole_http_response $response): bool
{
// ws rfc 规范中约定的验证过程
if (!isset($request->header['sec-websocket-key'])) {
// 需要 Sec-WebSocket-Key 如果没有拒绝握手
return false;
}
if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $request->header['sec-websocket-key'])
|| 16 !== strlen(base64_decode($request->header['sec-websocket-key']))
) {
//不接受握手
return false;
}
$key = base64_encode(sha1($request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$headers = array(
'Upgrade' => 'websocket',
'Connection' => 'Upgrade',
'Sec-WebSocket-Accept' => $key,
'Sec-WebSocket-Version' => '13',
'KeepAlive' => 'off',
);
if (isset($request->header['sec-websocket-protocol'])) {
$headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
}
// 发送验证后的header
foreach ($headers as $key => $val) {
$response->header($key, $val);
}
// 接受握手 还需要101状态码以切换状态
$response->status(101);
// fd 和 uuid 进行绑定
FdManager::getInstance()->bind($request->fd, $request->get['uuid']);
return true;
}

/**
* 自定义握手事件
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
private function customHandShake(\swoole_http_request $request, \swoole_http_response $response)
{
return true;
}
}

Webcoket解析类App/WebSocket/WebSocketParser.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php


namespace App\WebSocket;
use EasySwoole\Socket\AbstractInterface\ParserInterface;
use EasySwoole\Socket\Bean\Caller;
use EasySwoole\Socket\Bean\Response;

class WebSocketParser implements ParserInterface
{
public function decode($raw, $client): ?Caller
{
// TODO: Implement decode() method.
$caller = new Caller;
if ($raw !== 'PING') {
$payload = json_decode($raw, true);
$class = isset($payload['controller']) ? $payload['controller'] : 'index';
$action = isset($payload['action']) ? $payload['action'] : 'actionNotFound';
$params = isset($payload['params']) ? (array)$payload['params'] : [];
$controllerClass = "\\App\\WebSocket\\Controller\\" . ucfirst($class);
if (!class_exists($controllerClass)) $controllerClass = "\\App\\WebSocket\\Controller\\Index";
$caller->setClient($caller);
$caller->setControllerClass($controllerClass);
$caller->setAction($action);
$caller->setArgs($params);
} else {
// 设置心跳执行的类和方法
$caller->setControllerClass(\App\WebSocket\Controller\Base::class);
$caller->setAction('heartbeat');
}
return $caller;
}

public function encode(Response $response, $client): ?string
{
// TODO: Implement encode() method.
return $response->getMessage();
}
}

注册 websocket服务 EasySwooleEvent.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static function mainServerCreate(EventRegister $register)
{
$conf = new \EasySwoole\Socket\Config();
//设置Dispatcher为WebSocket 模式
$conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);
try {
$conf->setParser(new \App\WebSocket\WebSocketParser());//设置解析器对象
$dispatch = new \EasySwoole\Socket\Dispatcher($conf);//创建Dispatcher对象并注入config对象
} catch (\Exception $e) {
Log::error($e->getMessage());
}
//给server注册相关事件在WebSocket模式下onMessage事件必须注册 并且交给Dispatcher对象处理
$register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) use ($dispatch) {
$dispatch->dispatch($server, $frame->data, $frame);
});
$websocketEvent = new \App\WebSocket\WebSocketEvent();
//自定义握手事件
$register->set(EventRegister::onHandShake, function (\swoole_http_request $request, \swoole_http_response $response) use ($websocketEvent) {
$websocketEvent->onHandShake($request, $response);
});
//自定义关闭事件
$register->set(EventRegister::onClose, function (\swoole_server $server, int $fd, int $reactorId) use ($websocketEvent) {
$websocketEvent->onClose($server, $fd, $reactorId);
});

}

创建控制器目录,让easyswoole 的websocket服务像http服务那样调用方便

基类控制器App/WebSocket/Controller/Base.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace App\WebSocket\Controller;
use EasySwoole\Socket\AbstractInterface\Controller;
/**
* Class Base
* @package App\WebSocket\Controller
*/
class Base extends Controller
{
/**
* 心跳执行的方法
* 该方法建议 迁移到 基类控制器 Base 中
* 推荐使用 easyswoole 自带的websocket客户端调试
* http://www.easyswoole.com/wstool.html
*/
public function heartbeat()
{
$this->response()->setMessage('心跳 heartbeat');
}

/**
* @param string|null $actionName
*/
protected function actionNotFound(?string $actionName)
{
$this->response()->setMessage($actionName . ' not find'); // 推送消息
}
}

测试控制器App/WebSocket/Controller/Test.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace App\WebSocket\Controller;
class Test extends Base
{
public function index()
{
$fd = $this->caller()->getClient()->getFd();// 请求用户的fd
$data = $this->caller()->getArgs(); // 获取请求参数
//
\EasySwoole\EasySwoole\ServerManager::getInstance()->getSwooleServer()->worker_id

//发送响应消息
$this->response()->setMessage("响应消息");

$server = ServerManager::getInstance()->getSwooleServer();
//推送消息
$server->push($fd, “要推送的消息”);
}
}