Laravel 中使用 swoole 项目实战开发案例二 (后端主动分场景给界面推送消息)
推荐阅读: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 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
后端界面点击按钮 2