1. 安装socket包
composer require easyswoole/socket
2.注册服务
public static function mainServerCreate(EventRegister $register)
{
//创建一个 Dispatcher 配置
$conf = new \EasySwoole\Socket\Config();
//设置Dispatcher为WebSocket 模式
$conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);
try {
$conf->setParser(new WebSocketParser());//设置解析器对象
$dispatch = new Dispatcher($conf); //创建Dispatcher对象并注入config对象
} catch (\Exception $e) {
}
//给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 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);
});
}
3.配置服务类型
return [
'SERVER_NAME' => "YZGWCanteenServer",
'MAIN_SERVER' => [
'LISTEN_ADDRESS' => '0.0.0.0',
'PORT' => 9501,
'SERVER_TYPE' => EASYSWOOLE_WEB_SOCKET_SERVER, //可选为 EASYSWOOLE_SERVER EASYSWOOLE_WEB_SERVER EASYSWOOLE_WEB_SOCKET_SERVER
'SOCK_TYPE' => SWOOLE_TCP,
'RUN_MODEL' => SWOOLE_PROCESS,
'SETTING' => [
'worker_num' => 8,
'reload_async' => true,
'max_wait_time' => 3
],
'TASK' => [
'workerNum' => 4,
'maxRunningNum' => 128,
'timeout' => 15
]
],
4.自定义时事件
<?php
/**
* Description:this is description
* User:gan
* Date:2021/9/23
* Time:11:42 上午
*/
namespace App\WebSocket;
/**
* Class WebSocketEvent
* 此类是 WebSocet 中一些非强制的自定义事件处理
* @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)
{
$time = time();
echo "{$request->fd}创建连接事件 : {$time} \n";
/** 此处自定义握手规则 返回 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_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
protected function customHandShake(\swoole_http_request $request, \swoole_http_response $response): bool
{
/**
* 这里可以通过 http request 获取到相应的数据
* 进行自定义验证后即可
* (注) 浏览器中 JavaScript 并不支持自定义握手请求头 只能选择别的方式 如get参数
*/
$headers = $request->header;
$cookie = $request->cookie;
// if (如果不满足我某些自定义的需求条件,返回false,握手失败) {
// return false;
// }
return true;
}
/**
* 关闭事件
* 所有客户端关闭时触发的方法
* @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) {
$time = time();
echo "{$fd}触发关闭事件 : {$time} \n";
print_r($info);
/**
* 判断连接是否是 server 主动关闭
* 参见 https://wiki.swoole.com/wiki/page/p-event/onClose.html
*/
if ($reactorId < 0) {
echo "server close \n";
}
}
}
/**
* 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 如果没有拒绝握手
var_dump('shake fai1 3');
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']))
) {
//不接受握手
//var_dump('shake fai1 4');
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);
//var_dump('shake success at fd :' . $request->fd);
return true;
}
}
5.自定义解析器
<?php
/**
* Description:this is description
* User:ligan
* Date:2021/9/23
* Time:11:42 上午
*/
namespace App\WebSocket;
use EasySwoole\Socket\AbstractInterface\ParserInterface;
use EasySwoole\Socket\Bean\Caller;
use EasySwoole\Socket\Bean\Response;
use EasySwoole\Socket\Client\WebSocket as WebSocketClient;
class WebSocketParser implements ParserInterface
{
/**
* 解码上来的消息
* @param string $raw 消息内容
* @param WebSocketClient $client 当前的客户端
* @return Caller|null
*/
public function decode($raw, $client): ?Caller
{
$caller = new Caller;
// 聊天消息 {"controller":"broadcast","action":"roomBroadcast","params":{"content":"111"}}
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\Index::class);
$caller->setAction('heartbeat');
}
return $caller;
}
/**
* 打包下发的消息
* @param Response $response 控制器返回的响应
* @param WebSocketClient $client 当前的客户端
* @return string|null
*/
public function encode(Response $response, $client): ?string
{
return $response->getMessage();
}
}
6.业务代码
Base.php
<?php
/**
* Description:this is description
* User:ligan
* Date:2021/9/23
* Time:11:43 上午
*/
namespace App\WebSocket\Controller;
use EasySwoole\Socket\AbstractInterface\Controller;
use EasySwoole\Socket\Client\WebSocket as WebSocketClient;
use Exception;
/**
* 基础控制器
* Class Base
* @package App\WebSocket\Controller
*/
class Base extends Controller
{
protected function actionNotFound(?string $actionName)
{
echo "您的请求 {$actionName} 不存在 ... \n";
}
protected function afterAction(?string $actionName)
{
echo "请求之后执行 \n";
}
}
Index.php
<?php
/**
* Description:this is description
* User:ligan
* Date:2021/9/23
* Time:11:43 上午
*/
namespace App\WebSocket\Controller;
class Index extends Base
{
/**
* 心跳执行的方法
* User:ligan
* Date:2021/9/23
* Time:11:57 上午
*/
public function heartbeat()
{
$this->response()->setMessage("PONG"); // 推送消息
}
public function index()
{
$fd = $this->caller()->getClient()->getFd(); // 请求用户的fd
$data = $this->caller()->getArgs(); // 获取请求参数
$this->response()->setMessage('your fd is ' . $fd . json_encode($data)); // 推送消息
}
}
服务端业务如果想要推送到客户端,根据fd的映射关系,然后直接push到指定的fd。
public function index()
{
//TODO: $fd需要查询map关系,redis等存储
$fd = 1;
$server = ServerManager::getInstance()->getSwooleServer();
$fdInfo = $server->getClientInfo($fd);
if ($fdInfo["websocket_status"]) {
$server->push($fd, "hello http to websocket!");
}
}
如何遍历全部链接
use EasySwoole\EasySwoole\ServerManager;
$server = ServerManager::getInstance()->getSwooleServer();
$start_fd = 0;
while(true)
{
$conn_list = $server->getClientList($start_fd, 10);
if ($conn_list===false or count($conn_list) === 0)
{
echo "finish\n";
break;
}
$start_fd = end($conn_list);
var_dump($conn_list);
foreach($conn_list as $fd)
{
$server->send($fd, "broadcast");
}
}
https://wiki.swoole.com/wiki/page/p-connection_list.html
如何获取链接信息
use EasySwoole\EasySwoole\ServerManager;
$server = ServerManager::getInstance()->getSwooleServer();
$fdinfo = $server->getClientInfo($fd);