推薦閱讀:Laravel 中使用 swoole 項目實戰開發案例一 (建立 swoole 和前端通信) 需求分析 我們假設有一個需求,我在後端點擊按鈕 1,首頁彈出 “後端觸發了按鈕 1”。後端點了按鈕 2,列表頁彈出 “後端觸發了按鈕 2”。做到根據不同場景推送到不同頁面。 代碼思路 Swool ...
推薦閱讀:Laravel 中使用 swoole 項目實戰開發案例一 (建立 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 方法。在其裡面實現自己的邏輯。為防止過多的 ifelse 以及 foreach 操作,我們採用的是閉包,call_user_func 等方法實現如下
1 public function onRequest($request,$response) 2 { 3 if ($this->checkAccess("", $request)) { 4 $param = $request->get; 5 // 分發處理請求邏輯 6 if (isset($param['func'])) { 7 if (method_exists($this,$param['func'])) { 8 call_user_func([$this,$param['func']],$request); 9 } 10 } 11 } 12 }// 往首頁推送邏輯處理 13 public function pushHomeLogic($request) 14 { 15 $callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) { 16 if ($aContent && $aContent['page'] == "home") { 17 $aRes['message'] = "後端按了按鈕1"; 18 $aRes['code'] = "200"; 19 $oSwoole::$server->push($fd,xss_json($aRes)); 20 } 21 }; 22 $this->eachFdLogic($callback); 23 }
完整代碼
swool 腳本代碼邏輯
1 <?php 2 3 namespace App\Console\Commands; 4 5 use Closure; 6 use Illuminate\Console\Command; 7 use Illuminate\Support\Facades\Redis; 8 9 class SwooleDemo extends Command 10 { 11 // 命令名稱 12 protected $signature = 'swoole:demo'; 13 // 命令說明 14 protected $description = '這是關於swoole websocket的一個測試demo'; 15 // swoole websocket服務 16 private static $server = null; 17 18 public function __construct() 19 { 20 parent::__construct(); 21 } 22 23 // 入口 24 public function handle() 25 { 26 $this->redis = Redis::connection('websocket'); 27 $server = self::getWebSocketServer(); 28 $server->on('open',[$this,'onOpen']); 29 $server->on('message', [$this, 'onMessage']); 30 $server->on('close', [$this, 'onClose']); 31 $server->on('request', [$this, 'onRequest']); 32 $this->line("swoole服務啟動成功 ..."); 33 $server->start(); 34 } 35 36 // 獲取服務 37 public static function getWebSocketServer() 38 { 39 if (!(self::$server instanceof \swoole_websocket_server)) { 40 self::setWebSocketServer(); 41 } 42 return self::$server; 43 } 44 // 服務處始設置 45 protected static function setWebSocketServer():void 46 { 47 self::$server = new \swoole_websocket_server("0.0.0.0", 9502); 48 self::$server->set([ 49 'worker_num' => 1, 50 'heartbeat_check_interval' => 60, // 60秒檢測一次 51 'heartbeat_idle_time' => 121, // 121秒沒活動的 52 ]); 53 } 54 55 // 打開swoole websocket服務回調代碼 56 public function onOpen($server, $request) 57 { 58 if ($this->checkAccess($server, $request)) { 59 self::$server->push($request->fd,xss_json(["code"=>200,"message"=>"打開swoole服務成功"])); 60 } 61 } 62 // 給swoole websocket 發送消息回調代碼 63 public function onMessage($server, $frame) 64 { 65 66 } 67 // http請求swoole websocket 回調代碼 68 public function onRequest($request,$response) 69 { 70 if ($this->checkAccess("", $request)) { 71 $param = $request->get; 72 // 分發處理請求邏輯 73 if (isset($param['func'])) { 74 if (method_exists($this,$param['func'])) { 75 call_user_func([$this,$param['func']],$request); 76 } 77 } 78 } 79 } 80 81 // websocket 關閉回調代碼 82 public function onClose($serv,$fd) 83 { 84 $this->redis->hdel('swoole:fds', $fd); 85 $this->line("客戶端 {$fd} 關閉"); 86 } 87 88 // 校驗客戶端連接的合法性,無效的連接不允許連接 89 public function checkAccess($server, $request):bool 90 { 91 $bRes = true; 92 if (!isset($request->get) || !isset($request->get['token'])) { 93 self::$server->close($request->fd); 94 $this->line("介面驗證欄位不全"); 95 $bRes = false; 96 } else if ($request->get['token'] != 123456) { 97 $this->line("介面驗證錯誤"); 98 $bRes = false; 99 } 100 $this->storeUrlParamToRedis($request); 101 return $bRes; 102 } 103 104 // 將每個界面打開websocket的url 存儲起來 105 public function storeUrlParamToRedis($request):void 106 { 107 // 存儲請求url帶的信息 108 $sContent = json_encode( 109 [ 110 'page' => $request->get['page'], 111 'fd' => $request->fd, 112 ], true); 113 $this->redis->hset("swoole:fds", $request->fd, $sContent); 114 } 115 116 /** 117 * @param $request 118 * @see 迴圈邏輯處理 119 */ 120 public function eachFdLogic(Closure $callback = null) 121 { 122 foreach (self::$server->connections as $fd) { 123 if (self::$server->isEstablished($fd)) { 124 $aContent = json_decode($this->redis->hget("swoole:fds",$fd),true); 125 $callback($aContent,$fd,$this); 126 } else { 127 $this->redis->hdel("swoole:fds",$fd); 128 } 129 } 130 } 131 // 往首頁推送邏輯處理 132 public function pushHomeLogic($request) 133 { 134 $callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) { 135 if ($aContent && $aContent['page'] == "home") { 136 $aRes['message'] = "後端按了按鈕1"; 137 $aRes['code'] = "200"; 138 $oSwoole::$server->push($fd,xss_json($aRes)); 139 } 140 }; 141 $this->eachFdLogic($callback); 142 } 143 // 往列表頁推送邏輯處理 144 public function pushListLogic($request) 145 { 146 $callback = function (array $aContent,int $fd,SwooleDemo $oSwoole)use($request) { 147 if ($aContent && $aContent['page'] == "list") { 148 $aRes['message'] = "後端按了按鈕2"; 149 $aRes['code'] = "200"; 150 $oSwoole::$server->push($fd,xss_json($aRes)); 151 } 152 }; 153 $this->eachFdLogic($callback); 154 } 155 156 // 啟動websocket服務 157 public function start() 158 { 159 self::$server->start(); 160 } 161 } 162 控制器代碼 163 164 <?php 165 166 namespace App\Http\Controllers; 167 168 use Illuminate\Http\Request; 169 use Illuminate\Support\Facades\Redis; 170 class TestController extends Controller 171 { 172 // 首頁 173 public function home() 174 { 175 return view("home"); 176 } 177 // 列表 178 public function list() 179 { 180 return view("list"); 181 } 182 // 後端控制 183 public function back() 184 { 185 if (request()->method() == 'POST') { 186 $this->curl_get($this->getUrl()); 187 return json_encode(['code'=>200,"message"=>"成功"]); 188 } else { 189 return view("back"); 190 } 191 192 } 193 // 獲取要請求swoole websocet服務地址 194 public function getUrl():string 195 { 196 // 功能變數名稱 埠 請求swoole服務的方法 197 $sBase = request()->server('HTTP_HOST'); 198 $iPort = 9502; 199 $sFunc = request()->post('func'); 200 $sPage = "back"; 201 return $sBase.":".$iPort."?func=".$sFunc."&token=123456&page=".$sPage; 202 } 203 // curl 推送 204 public function curl_get(string $url):string 205 { 206 $ch_curl = curl_init(); 207 curl_setopt ($ch_curl, CURLOPT_TIMEOUT_MS, 3000); 208 curl_setopt($ch_curl, CURLOPT_SSL_VERIFYPEER, 0); 209 curl_setopt ($ch_curl, CURLOPT_HEADER,false); 210 curl_setopt($ch_curl, CURLOPT_HTTPGET, 1); 211 curl_setopt($ch_curl, CURLOPT_RETURNTRANSFER,true); 212 curl_setopt ($ch_curl, CURLOPT_URL,$url); 213 $str = curl_exec($ch_curl); 214 curl_close($ch_curl); 215 return $str; 216 } 217 }
頁面 js 代碼
後端控制頁
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>後端界面</title> 6 <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 7 </head> 8 <body> 9 <button class="push" data-func="pushHomeLogic">按鈕1</button> 10 <button class="push" data-func="pushListLogic">按鈕2</button> 11 </body> 12 <script src="{{ asset("/vendor/tw/global/jQuery/jquery-2.2.3.min.js")}} "></script> 13 <script> 14 $(function () { 15 $(".push").on('click',function(){ 16 var func = $(this).attr('data-func').trim(); 17 ajaxGet(func) 18 }) 19 function ajaxGet(func) { 20 url = "{{route('back')}}"; 21 token = "{{csrf_token()}}"; 22 $.ajax({ 23 url: url, 24 type: 'post', 25 dataType: "json", 26 data:{func:func,_token:token}, 27 error: function (data) { 28 alert("伺服器繁忙, 請聯繫管理員!"); 29 return; 30 }, 31 success: function (result) { 32 33 }, 34 }) 35 } 36 37 }) 38 </script> 39 </html>
首頁
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>swoole首頁</title> 6 <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 7 </head> 8 <body> 9 <h1>這是首頁</h1> 10 </body> 11 <script> 12 var ws;//websocket實例 13 var lockReconnect = false;//避免重覆連接 14 var wsUrl = 'ws://{{$_SERVER["HTTP_HOST"]}}:9502?page=home&token=123456'; 15 16 function initEventHandle() { 17 ws.onclose = function () { 18 reconnect(wsUrl); 19 }; 20 ws.onerror = function () { 21 reconnect(wsUrl); 22 }; 23 ws.onopen = function () { 24 //心跳檢測重置 25 heartCheck.reset().start(); 26 }; 27 ws.onmessage = function (event) { 28 //如果獲取到消息,心跳檢測重置 29 //拿到任何消息都說明當前連接是正常的 30 var data = JSON.parse(event.data); 31 if (data.code == 200) { 32 console.log(data.message) 33 } 34 heartCheck.reset().start(); 35 } 36 } 37 createWebSocket(wsUrl); 38 /** 39 * 創建鏈接 40 * @param url 41 */ 42 function createWebSocket(url) { 43 try { 44 ws = new WebSocket(url); 45 initEventHandle(); 46 } catch (e) { 47 reconnect(url); 48 } 49 } 50 function reconnect(url) { 51 if(lockReconnect) return; 52 lockReconnect = true; 53 //沒連接上會一直重連,設置延遲避免請求過多 54 setTimeout(function () { 55 createWebSocket(url); 56 lockReconnect = false; 57 }, 2000); 58 } 59 //心跳檢測 60 var heartCheck = { 61 timeout: 60000,//60秒 62 timeoutObj: null, 63 serverTimeoutObj: null, 64 reset: function(){ 65 clearTimeout(this.timeoutObj); 66 clearTimeout(this.serverTimeoutObj); 67 return this; 68 }, 69 start: function(){ 70 var self = this; 71 this.timeoutObj = setTimeout(function(){ 72 //這裡發送一個心跳,後端收到後,返回一個心跳消息, 73 //onmessage拿到返回的心跳就說明連接正常 74 ws.send("heartbeat"); 75 self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明後端主動斷開了 76 ws.close();//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次 77 }, self.timeout); 78 }, this.timeout); 79 }, 80 header:function(url) { 81 window.location.href=url 82 } 83 84 } 85 </script> 86 </html>
列表頁面
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>swoole列表頁</title> 6 <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 7 </head> 8 <body> 9 <h1>swoole列表頁</h1> 10 </body> 11 <script> 12 var ws;//websocket實例 13 var lockReconnect = false;//避免重覆連接 14 var wsUrl = 'ws://{{$_SERVER["HTTP_HOST"]}}:9502?page=list&token=123456'; 15 16 function initEventHandle() { 17 ws.onclose = function () { 18 reconnect(wsUrl); 19 }; 20 ws.onerror = function () { 21 reconnect(wsUrl); 22 }; 23 ws.onopen = function () { 24 //心跳檢測重置 25 heartCheck.reset().start(); 26 }; 27 ws.onmessage = function (event) { 28 //如果獲取到消息,心跳檢測重置 29 //拿到任何消息都說明當前連接是正常的 30 var data = JSON.parse(event.data); 31 if (data.code == 200) { 32 console.log(data.message) 33 } 34 heartCheck.reset().start(); 35 } 36 } 37 createWebSocket(wsUrl); 38 /** 39 * 創建鏈接 40 * @param url 41 */ 42 function createWebSocket(url) { 43 try { 44 ws = new WebSocket(url); 45 initEventHandle(); 46 } catch (e) { 47 reconnect(url); 48 } 49 } 50 function reconnect(url) { 51 if(lockReconnect) return; 52 lockReconnect = true; 53 //沒連接上會一直重連,設置延遲避免請求過多 54 setTimeout(function () { 55 createWebSocket(url); 56 lockReconnect = false; 57 }, 2000); 58 } 59 //心跳檢測 60 var heartCheck = { 61