最近使用 swoole 做了專案,裡面設計推送資訊給介面前端,和無登陸使用者的狀態監控,以下是本人從中獲取的一點心得,有改進的地方請留言評論。
需求分析
我們假設有一個需求,我在後端點選按鈕1,首頁彈出“後端觸發了按鈕1”。後端點了按鈕2,列表頁彈出“後端觸發了按鈕2”。做到根據不同場景推送到不同頁面。
程式碼思路
-
Swoole fd
客戶端瀏覽器開啟或者重新整理介面,在swoole服務會生成一個程式控制程式碼 fd ,每次瀏覽器頁面有開啟連結websocket的js程式碼,便會生成,每次重新整理的時候,會關閉之前開啟的 fd,重新生成一個新的,關閉介面的時候會生成一個新的。swoole的 fd生成規則是從1開始遞增。
-
Redis Hash儲存 fd
我們建立一個key為swoole:fds redis雜湊型別資料,fd 為hash的欄位,每個欄位的值我們儲存前端websocket請求的url引數資訊(根據業務複雜度自己靈活變通,我在專案中會在url帶上sessionId)。每次連結開啟swoole服務的時候我們儲存其資訊,每次關閉頁面時候我們清除其欄位。在redis儲存如下
-
觸發分場景推送
在介面上當進行了觸發操作的時候,通過後臺curl請求swoole http服務,swoole http服務根據你向我傳遞的引數分發給對應的邏輯處理。如curl請求
127.0.0.1:9502page=back&func=pushHomeLogic&token=123456
我們可以根據傳入的func引數,在後臺分發給對應邏輯處理。如分發給pushHomeLogic方法。在其裡面實現自己的邏輯。為防止過多的if else 以及 foreach 操作,我們採用的是閉包,call_user_func等方法實現如下public function onRequest($request,$response) { if ($this->checkAccess("", $request)) { $param = $request->get; // 分發處理請求邏輯 if (isset($param['func'])) { if (method_exists($this,$param['func'])) { call_user_func([$this,$param['func']],$request); } } } }// 往首頁推送邏輯處理 public function pushHomeLogic($request) { $callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) { if ($aContent && $aContent['page'] == "home") { $aRes['message'] = "後端按了按鈕1"; $aRes['code'] = "200"; $oSwoole::$server->push($fd,xss_json($aRes)); } }; $this->eachFdLogic($callback); }
完整程式碼
swool指令碼程式碼邏輯
<?php
namespace App\Console\Commands;
use Closure;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class SwooleDemo extends Command
{
// 命令名稱
protected $signature = 'swoole:demo';
// 命令說明
protected $description = '這是關於swoole websocket的一個測試demo';
// swoole websocket服務
private static $server = null;
public function __construct()
{
parent::__construct();
}
// 入口
public function handle()
{
$this->redis = Redis::connection('websocket');
$server = self::getWebSocketServer();
$server->on('open',[$this,'onOpen']);
$server->on('message', [$this, 'onMessage']);
$server->on('close', [$this, 'onClose']);
$server->on('request', [$this, 'onRequest']);
$this->line("swoole服務啟動成功 ...");
$server->start();
}
// 獲取服務
public static function getWebSocketServer()
{
if (!(self::$server instanceof \swoole_websocket_server)) {
self::setWebSocketServer();
}
return self::$server;
}
// 服務處始設定
protected static function setWebSocketServer():void
{
self::$server = new \swoole_websocket_server("0.0.0.0", 9502);
self::$server->set([
'worker_num' => 1,
'heartbeat_check_interval' => 60, // 60秒檢測一次
'heartbeat_idle_time' => 121, // 121秒沒活動的
]);
}
// 開啟swoole websocket服務回撥程式碼
public function onOpen($server, $request)
{
if ($this->checkAccess($server, $request)) {
self::$server->push($request->fd,xss_json(["code"=>200,"message"=>"開啟swoole服務成功"]));
}
}
// 給swoole websocket 傳送訊息回撥程式碼
public function onMessage($server, $frame)
{
}
// http請求swoole websocket 回撥程式碼
public function onRequest($request,$response)
{
if ($this->checkAccess("", $request)) {
$param = $request->get;
// 分發處理請求邏輯
if (isset($param['func'])) {
if (method_exists($this,$param['func'])) {
call_user_func([$this,$param['func']],$request);
}
}
}
}
// websocket 關閉回撥程式碼
public function onClose($serv,$fd)
{
$this->redis->hdel('swoole:fds', $fd);
$this->line("客戶端 {$fd} 關閉");
}
// 校驗客戶端連線的合法性,無效的連線不允許連線
public function checkAccess($server, $request):bool
{
$bRes = true;
if (!isset($request->get) || !isset($request->get['token'])) {
self::$server->close($request->fd);
$this->line("介面驗證欄位不全");
$bRes = false;
} else if ($request->get['token'] != 123456) {
$this->line("介面驗證錯誤");
$bRes = false;
}
$this->storeUrlParamToRedis($request);
return $bRes;
}
// 將每個介面開啟websocket的url 儲存起來
public function storeUrlParamToRedis($request):void
{
// 儲存請求url帶的資訊
$sContent = json_encode(
[
'page' => $request->get['page'],
'fd' => $request->fd,
], true);
$this->redis->hset("swoole:fds", $request->fd, $sContent);
}
/**
* @param $request
* @see 迴圈邏輯處理
*/
public function eachFdLogic(Closure $callback = null)
{
foreach (self::$server->connections as $fd) {
if (self::$server->isEstablished($fd)) {
$aContent = json_decode($this->redis->hget("swoole:fds",$fd),true);
$callback($aContent,$fd,$this);
} else {
$this->redis->hdel("swoole:fds",$fd);
}
}
}
// 往首頁推送邏輯處理
public function pushHomeLogic($request)
{
$callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) {
if ($aContent && $aContent['page'] == "home") {
$aRes['message'] = "後端按了按鈕1";
$aRes['code'] = "200";
$oSwoole::$server->push($fd,xss_json($aRes));
}
};
$this->eachFdLogic($callback);
}
// 往列表頁推送邏輯處理
public function pushListLogic($request)
{
$callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) {
if ($aContent && $aContent['page'] == "list") {
$aRes['message'] = "後端按了按鈕2";
$aRes['code'] = "200";
$oSwoole::$server->push($fd,xss_json($aRes));
}
};
$this->eachFdLogic($callback);
}
// 啟動websocket服務
public function start()
{
self::$server->start();
}
}
控制器程式碼
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class TestController extends Controller
{
// 首頁
public function home()
{
return view("home");
}
// 列表
public function list()
{
return view("list");
}
// 後端控制
public function back()
{
if (request()->method() == 'POST') {
$this->curl_get($this->getUrl());
return json_encode(['code'=>200,"message"=>"成功"]);
} else {
return view("back");
}
}
// 獲取要請求swoole websocet服務地址
public function getUrl():string
{
// 域名 埠 請求swoole服務的方法
$sBase = request()->server('HTTP_HOST');
$iPort = 9502;
$sFunc = request()->post('func');
$sPage = "back";
return $sBase.":".$iPort."?func=".$sFunc."&token=123456&page=".$sPage;
}
// curl 推送
public function curl_get(string $url):string
{
$ch_curl = curl_init();
curl_setopt ($ch_curl, CURLOPT_TIMEOUT_MS, 3000);
curl_setopt($ch_curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt ($ch_curl, CURLOPT_HEADER,false);
curl_setopt($ch_curl, CURLOPT_HTTPGET, 1);
curl_setopt($ch_curl, CURLOPT_RETURNTRANSFER,true);
curl_setopt ($ch_curl, CURLOPT_URL,$url);
$str = curl_exec($ch_curl);
curl_close($ch_curl);
return $str;
}
}
頁面js程式碼
-
後端控制頁
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>後端介面</title> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> </head> <body> <button class="push" data-func="pushHomeLogic">按鈕1</button> <button class="push" data-func="pushListLogic">按鈕2</button> </body> <script src="{{ asset("/vendor/tw/global/jQuery/jquery-2.2.3.min.js")}} "></script> <script> $(function () { $(".push").on('click',function(){ var func = $(this).attr('data-func').trim(); ajaxGet(func) }) function ajaxGet(func) { url = "{{route('back')}}"; token = "{{csrf_token()}}"; $.ajax({ url: url, type: 'post', dataType: "json", data:{func:func,_token:token}, error: function (data) { alert("伺服器繁忙, 請聯絡管理員!"); return; }, success: function (result) { }, }) } }) </script> </html>
-
首頁
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>swoole首頁</title> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> </head> <body> <h1>這是首頁</h1> </body> <script> var ws;//websocket例項 var lockReconnect = false;//避免重複連線 var wsUrl = 'ws://{{$_SERVER["HTTP_HOST"]}}:9502?page=home&token=123456'; function initEventHandle() { ws.onclose = function () { reconnect(wsUrl); }; ws.onerror = function () { reconnect(wsUrl); }; ws.onopen = function () { //心跳檢測重置 heartCheck.reset().start(); }; ws.onmessage = function (event) { //如果獲取到訊息,心跳檢測重置 //拿到任何訊息都說明當前連線是正常的 var data = JSON.parse(event.data); if (data.code == 200) { console.log(data.message) } heartCheck.reset().start(); } } createWebSocket(wsUrl); /** * 建立連結 * @param url */ function createWebSocket(url) { try { ws = new WebSocket(url); initEventHandle(); } catch (e) { reconnect(url); } } function reconnect(url) { if(lockReconnect) return; lockReconnect = true; //沒連線上會一直重連,設定延遲避免請求過多 setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 2000); } //心跳檢測 var heartCheck = { timeout: 60000,//60秒 timeoutObj: null, serverTimeoutObj: null, reset: function(){ clearTimeout(this.timeoutObj); clearTimeout(this.serverTimeoutObj); return this; }, start: function(){ var self = this; this.timeoutObj = setTimeout(function(){ //這裡傳送一個心跳,後端收到後,返回一個心跳訊息, //onmessage拿到返回的心跳就說明連線正常 ws.send("heartbeat"); self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明後端主動斷開了 ws.close();//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次 }, self.timeout); }, this.timeout); }, header:function(url) { window.location.href=url } } </script> </html>
-
列表頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>swoole列表頁</title> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> </head> <body> <h1>swoole列表頁</h1> </body> <script> var ws;//websocket例項 var lockReconnect = false;//避免重複連線 var wsUrl = 'ws://{{$_SERVER["HTTP_HOST"]}}:9502?page=list&token=123456'; function initEventHandle() { ws.onclose = function () { reconnect(wsUrl); }; ws.onerror = function () { reconnect(wsUrl); }; ws.onopen = function () { //心跳檢測重置 heartCheck.reset().start(); }; ws.onmessage = function (event) { //如果獲取到訊息,心跳檢測重置 //拿到任何訊息都說明當前連線是正常的 var data = JSON.parse(event.data); if (data.code == 200) { console.log(data.message) } heartCheck.reset().start(); } } createWebSocket(wsUrl); /** * 建立連結 * @param url */ function createWebSocket(url) { try { ws = new WebSocket(url); initEventHandle(); } catch (e) { reconnect(url); } } function reconnect(url) { if(lockReconnect) return; lockReconnect = true; //沒連線上會一直重連,設定延遲避免請求過多 setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 2000); } //心跳檢測 var heartCheck = { timeout: 60000,//60秒 timeoutObj: null, serverTimeoutObj: null, reset: function(){ clearTimeout(this.timeoutObj); clearTimeout(this.serverTimeoutObj); return this; }, start: function(){ var self = this; this.timeoutObj = setTimeout(function(){ //這裡傳送一個心跳,後端收到後,返回一個心跳訊息, //onmessage拿到返回的心跳就說明連線正常 ws.send("heartbeat"); self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明後端主動斷開了 ws.close();//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次 }, self.timeout); }, this.timeout); }, header:function(url) { window.location.href=url } } </script> </html>
介面效果
後臺控制點選按鈕1
後端介面點選按鈕2