Laravel 中使用 swoole 项目实战开发案例二 (后端主动分场景给界面推送消息)
程序员文章站
2023-03-26 20:10:45
推荐阅读: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 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
上一篇: 无需QQ成为好友,直接启动QQ客户端聊天
下一篇: CORS