💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 安装服务 ~~~ composer require topthink/think-swoole=2.0.* ~~~ # 创建执行单文件 shurima.php ```php <?php class WebSocketServer { public $server; public $tcp; private $tag = 'swoole_user_sessions'; public function __construct() { $this->clearFds();// 开启服务前,清除之前建立的fd用户信息,验证信息保留 $this->server = new Swoole\WebSocket\Server("0.0.0.0", 9001, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL); $this->server->set([ //'ssl_cert_file' => '/www/server/panel/vhost/cert/hx.toushizhiku.com/fullchain.pem', //'ssl_key_file' => '/www/server/panel/vhost/cert/hx.toushizhiku.com/privkey.pem', 'ssl_cert_file' => __DIR__.'/config/fullchain.pem', 'ssl_key_file' => __DIR__.'/config/privkey.pem', 'heartbeat_idle_time' => 600,// 表示一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭 'heartbeat_check_interval' => 60,// 表示每60秒遍历一次 'worker_num' => 4, 'max_conn' => 50,// 最大连接数, 'log_file' => __DIR__.'/logs/swoole/log.txt', 'log_level' => 0, 'log_rotation' => SWOOLE_LOG_ROTATION_DAILY ]); $this->tcp = $this->server->listen("0.0.0.0", 9002, SWOOLE_SOCK_TCP); $this->tcp->set([ //'open_http_protocol' => true, ]); $this->server->on('WorkerStart', function ($server, $worker_id){ $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $server->redis = $redis; }); $this->server->on('handshake', function (\Swoole\Http\Request $request, \Swoole\Http\Response $response) { $redis = $this->server->redis; $tag = $this->tag; // websocket握手连接算法验证 $secWebSocketKey = $request->header['sec-websocket-key']; $patten = '#^[+/0-9A-Za-z]{21}[AQgw]==$#'; if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) { $response->end(); return false; } echo 'sec-key_'.$request->header['sec-websocket-key']."\n"; // 验证用户 $fd = $request->fd; $session_id = $request->cookie['PHPSESSID']; echo 'handshake_'.$fd.'_sess_'.$session_id.'_'.json_encode($request->server['remote_addr'])."\n"; if (!$fd || empty($session_id)) { $response->end(); return false; } if (!$redis->hExists($tag, $session_id)) { $response->end(); return false; } $sess_data = json_decode($redis->hGet($tag, $session_id), true); echo 'handshake_sess_'.json_encode($sess_data).'_time_'.time()."\n"; if (!is_array($sess_data) || !isset($sess_data['user_id']) || !isset($sess_data['create_time'])) { $response->end(); return false; } if ((time() - $sess_data['create_time']) > 86400 || time() < $sess_data['create_time']) { $response->end(); return false; } $sess_data['fd'] = $fd; $redis->hSet($tag, $session_id, json_encode($sess_data)); unset($sess_data, $fd, $session_id, $tag); $key = base64_encode( sha1( $request->header['sec-websocket-key'] . '344EAFA5-E858-257A-95CA-C7AB0DC85B22', true ) ); $headers = [ 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Accept' => $key, 'Sec-WebSocket-Version' => '13', ]; // WebSocket connection to 'ws://127.0.0.1:9001/' // failed: Error during WebSocket handshake: // Response must not include 'Sec-WebSocket-Protocol' header if not present in request: websocket if (isset($request->header['sec-websocket-protocol'])) { $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol']; } foreach ($headers as $key => $val) { $response->header($key, $val); } // onHandShake 中必须调用 response->status() 设置状态码为 101 并调用 response->end() 响应,否则会握手失败 $response->status(101); $response->end(); }); $this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) { echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; $server->push($frame->fd, json_encode(['msg' => 'Hello, welcome.'])); }); $this->tcp->on('receive', function ($server, $fd, $reactor_id, $data) { echo 'receive_'.json_encode($fd).'_'.json_encode($data, 320).'_'.json_encode($reactor_id)."\n"; // 仅遍历 9001 端口的连接,因为是用的$server,不是$tcp // 获取tcp连接的信息 $data = json_decode($data, true); if (!empty($data) && isset($data['title']) && isset($data['user_ids'])) { $websocket = $server->ports[0]; $redis = $server->redis; $user_data = $redis->hGetAll($this->tag); $fds = []; $tmp = []; if (is_array($user_data)) { foreach ($user_data as $key => $user) { echo 'receive_user_'.$key.'_'.json_encode($user)."\n"; $tmp = json_decode($user, true); $create_time = !empty($tmp) && is_array($tmp) && isset($tmp['create_time']) ? $tmp['create_time'] : 0; if (!isset($tmp['user_id']) || (time() - $create_time > 86400) || time() < $create_time) { $redis->hDel($this->tag, $key); } if (isset($tmp['fd']) && in_array($tmp['user_id'], $data['user_ids'])) { array_push($fds, $tmp['fd']); } } } foreach ($websocket->connections as $_fd) { if (!in_array($_fd, $fds)) { continue; } if ($server->exist($_fd)) { $server->push($_fd, json_encode(['title' => $data['title']], 320)); } } unset($user_data, $fds, $tmp); // 对端不接收信息直接关闭,不再回复 //$server->send($fd, json_encode(['code' => 200, 'msg' => 'success'])); } else { // 对端不接收信息直接关闭,不再回复 //$server->send($fd, json_encode(['code' => 0, 'msg' => 'params less'])); } }); $this->server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); $this->server->on('WorkerStop', function ($server, $worker_id){ $server->redis->close(); }); $this->server->start(); } public function clearFds() { $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $tag = $this->tag; if (!$redis->exists($tag)) { $redis->close(); return true; } $datas = $redis->hGetAll($tag); if (is_array($datas)) { foreach ($datas as $key => $data) { $tmp = json_decode($data, true); $create_time = !empty($tmp) && is_array($tmp) && isset($tmp['create_time']) ? $tmp['create_time'] : 0; if (!isset($tmp['user_id']) || (time() - $create_time > 86400) || time() < $create_time) { $redis->hDel($tag, $key); } unset($tmp['fd']); $redis->hSet($tag, $key, json_encode($tmp)); } } $redis->close(); return true; } } new WebSocketServer(); ``` 命令行执行: > php shurima.php # 前端js ~~~ <script> var url = 'wss://aaa.test.com:9001'; var websocket, timer, max = 500, count = 0; var lockConnect = false; var connection = function () { websocket = new WebSocket(url); websocket.onopen = onopen; websocket.onmessage = onmessage; websocket.onclose = onclose; websocket.onerror = onerror; } var onopen = function () { console.log("Connected to WebSocket server."); websocket.send("Hello!"); ping(); } var onmessage = function (evt) { console.log('Retrieved data from server: ' + evt.data); if (evt.data.length) { var msg_data = JSON.parse(evt.data); console.log('message2', msg_data, typeof msg_data) if (msg_data.title == 'undefined' || msg_data.title == null || msg_data.title == '') { console.log('message', msg_data.title) return false; } notifyMsg('光年分配任务', msg_data.title); } ping(); } var onclose = function () { console.log("connection is closed"); reconnection(); } var onerror = function (evt, e) { console.log('Error occured: ' + evt, e); reconnection(); } var pingCheck, pongCheck; var ping = function () { pingCheck && clearTimeout(pingCheck); pongCheck && clearTimeout(pongCheck); pingCheck = setTimeout(function () { websocket.send('ping'); pongCheck = setTimeout(function () { websocket.close(); }, 3500) }, 60000); } var reconnection = function () { // 0 CONNECTING,1 OPEN,2 CLOSING,3 CLOSED if (lockConnect) { return false; } lockConnect = true; count = count + 1; console.log("reconnection " + count + " times and readyState is " + websocket.readyState); timer && clearTimeout(timer); if (count >= max || websocket.readyState == 1) { clearTimeout(timer); count = 0; } else { timer = setTimeout(function () { connection(); lockConnect = false; }, 30000); } } connection(); function notifyMsg(title, content) { var options = { dir: "auto", // 文字方向 body: content, requireInteraction: true, tag: "notification", renotify: true, icon: "https://file.toushivip.com/msg2.png", }; // 先检查浏览器是否支持 if (!window.Notification) { console.log('浏览器不支持通知'); } else { console.log('permission', Notification.permission); // 检查用户曾经是否同意接受通知 if (Notification.permission === 'granted') { var notification = new Notification(title, options); // 显示通知 notification.onclick = function (event) { event.preventDefault(); // prevent the browser from focusing the Notification's tab window.open('/portal/distribution'); } } else if (Notification.permission === 'default') { // 用户还未选择,可以询问用户是否同意发送通知 Notification.requestPermission().then(permission => { if (permission === 'granted') { var notification = new Notification(title, options); // 显示通知 notification.onclick = function (event) { event.preventDefault(); window.open('/portal/distribution'); } } else if (permission === 'default') { console.warn('用户关闭授权 未刷新页面之前 可以再次请求授权'); } else { // denied console.log('用户拒绝授权 不能显示通知'); } }); } else { // denied 用户拒绝 console.log('用户曾经拒绝显示通知'); } } } </script> ~~~