合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
# 完整功能体验 查看源代码,有完整的通讯流程实现。 [https://iyuu.cn](https://www.iyuu.cn/usr/index.html) 发表在自己的博客 [https://www.iyuu.cn/archives/202/](https://www.iyuu.cn/archives/202/)  #说明 此功能是利用微信公众号带参数二维码,实现扫码识别用户,并且实时通知前端扫码状态,并非ajax轮询!从而进行后续的其他业务逻辑。 # 工作流程详解  1. 用workerman框架,编写websocket服务后端监听`2129端口`,进程启动同时再监听一个内部通讯`5678端口`,`2129端口`等待前端页面发起连接:`https://www.iyuu.cn/usr/index.html`;  2. 用户进入前端页面,自动连接`wss://www.iyuu.cn:2129`;  3. 用户点击`获取二维码`,请求二维码生成接口:`https://www.iyuu.cn/qrcode`,返回二维码参数:  `json {"ticket":"gQH47zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycTMtdzlMVEhlYzIxcF9jQU50MWsAAgQHjGRdAwR4AAAA","expire_seconds":120,"uid":1735536450}  `   注:uid通过函数`rand(1,4294967200)`生成并查询缓存,确保唯一后放入Redis缓存。  4. 把`二维码参数`,转发到websocket服务`wss://www.iyuu.cn:2129`,websocket服务保存转发来的信息建立映射关系;  5. 显示二维码:[https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={data.ticket},用户扫码](https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={data.ticket}%EF%BC%8C%E7%94%A8%E6%88%B7%E6%89%AB%E7%A0%81);  6. 微信开发者接口会收到扫码结果,获取到场景值ID;  7. 根据场景值ID从Redis缓存取出ticket校验通过,执行业务逻辑(登录、绑定、解绑、积分等等),并通过`5678端口`实时通知用户扫码后的处理结果。 # 完整Websocket服务代码 ~~~php <?php use Workerman\Worker; use Workerman\Lib\Timer; define("APP_PATH", dirname(__FILE__)); // 心跳间隔40秒 define('HEARTBEAT_TIME', 40); require_once __DIR__ . '/../../vendor/autoload.php'; require_once APP_PATH . '/Library/Function.php'; $context = array( 'ssl' => array( // 请使用绝对路径 'local_cert' => __DIR__ . '/../../Cert/www.iyuu.cn.crt', 'local_pk' => __DIR__ . '/../../Cert/www.iyuu.cn.key', 'verify_peer' => false, //'allow_self_signed' => true, //如果是自签名证书需要开启此选项 ) ); $worker = new Worker('websocket://0.0.0.0:2129', $context); $worker->transport = 'ssl'; $worker->name = 'WebSocket'; /* * 注意这里进程数必须设置为1,否则会报端口占用错误 * (php 7可以设置进程数大于1,前提是$inner_text_worker->reusePort=true) */ $worker->count = 1; // 新增加一个属性,用来保存uid到connection的映射(uid是用户id或者客户端唯一标识) $worker->uidConnections = array(); // 当有客户端连接时 $worker->onConnect = function($connection) { /* //定时10秒关闭这个链接,需要10秒内发认证并删除定时器阻止关闭连接的执行 $connection->auth_timer_id = Timer::add(10, function(){ $connection->close(); }); Timer::del($connection->auth_timer_id); */ }; // worker进程启动后创建一个text Worker以便打开一个内部通讯端口 $worker->onWorkerStart = function($worker) { sc('WebSocket服务进程启动成功!'); // 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符 $inner_text_worker = new Worker('text://0.0.0.0:5678'); $inner_text_worker->onMessage = function($connection, $buffer) { global $worker; if (empty($buffer)) return; // $data数组格式,里面有uid,表示向那个uid的页面推送数据 $data = json_decode($buffer, true); if (isset($data['uid'])) { $uid = $data['uid']; //uid + ticket双重安全验证(防止前端冒用随机uid) $data['ticket'] = isset($data['ticket'])&&$data['ticket'] ? $data['ticket'] : ''; $conn = $worker->uidConnections[$uid]; $ticket = isset($conn->ticket)&&$conn->ticket ? $conn->ticket : ''; if($data['ticket'] != $ticket){ return; } // 通过workerman,向uid的页面推送数据 $ret = sendMessageByUid($uid, $buffer); // 返回推送结果 $connection->send($ret ? 'ok' : 'fail'); } return; }; // ## 执行监听 ## $inner_text_worker->listen(); // 进程启动后设置一个每秒运行一次的定时器 Timer::add(1, function()use($worker){ $time_now = time(); foreach($worker->uidConnections as $connection) { // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间 if (empty($connection->lastMessageTime)) { $connection->lastMessageTime = $time_now; continue; } // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接 if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) { if(isset($connection->uid)) { // 连接断开时删除映射 unset($worker->uidConnections[$connection->uid]); } $connection->close(); } } }); //每天重启进程 Timer::add(86400, function()use($worker) { sc('WebSocket服务进程定时重启任务,执行成功!'); Worker::stopAll(); }); }; // 当有客户端发来消息时执行的回调函数 $worker->onMessage = function($connection, $data) { global $worker; // 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间 $connection->lastMessageTime = time(); // 客户端传递的是json数据 if (empty($data)) return; $message = json_decode($data, true); if(empty($message)) return; if(isset($message['cmd'])) { // 根据类型执行不同的业务 switch($message['cmd']) { case 'ping': return; case 'login': return; case 'sms': return; case 'mail': return; default: return; } }else{ // 判断当前客户端是否已经验证,即是否设置了uid if(isset($connection->uid)) { //上次uid和ticket过期 if (isset($message['uid']) && ($message['uid']!=$connection->uid)) { unset($worker->uidConnections[$connection->uid]); } } if (isset($message['uid']) && $message['uid']) { // 没验证的话把第一个包当做uid $connection->uid = $message['uid']; if (isset($message['ticket'])) { //带参数二维码的ticket $connection->ticket = $message['ticket']; } /* 保存uid到connection的映射,这样可以方便的通过uid查找connection, * 实现针对特定uid推送数据 */ $worker->uidConnections[$connection->uid] = $connection; $connection->send($data); return; } else { //不带uid的消息 # code... } } }; // 当有客户端连接断开时 $worker->onClose = function($connection) { global $worker; if(isset($connection->uid)) { // 连接断开时删除映射 unset($worker->uidConnections[$connection->uid]); } }; // 进程关闭时 $worker->onWorkerStop = function($worker) { //通知运维人员 //sc('WebSocket服务进程退出,如非定时重启,请检查!'); }; // 针对uid推送数据 function sendMessageByUid($uid, $message) { global $worker; if(isset($worker->uidConnections[$uid])) { $connection = $worker->uidConnections[$uid]; $connection->send($message); return true; } return false; } // 如果不是在根目录启动,则运行runAll方法 if(!defined('GLOBAL_START')) { Worker::runAll(); } ~~~ ## 前端页面关键代码 ~~~html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>微信公众号模板消息通知Token申请页 - 大卫科技blog www.iyuu.cn</title> <meta name="keywords" content="大卫科技blog,www.iyuu.cn" /> <meta name="description" content="微信公众号模板消息通知Token申请页" /> <meta name="copyright" content="海南大卫电子科技有限公司" /> <meta name="author" content="大卫" /> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> </head> <body> <div id="panel"> <div id="header"> <h1>微信公众号模板消息通知<span>Token申请页</span></h1> <noscript><h1>你的浏览器不支持 JavaScript,请启用 JavaScript 后访问。</h1></noscript> <address>制作 by <a href="http://www.iyuu.cn/">大卫科技blog</a></address> </div> <div id="token" style="display: none;"></div> <div id="qrcode">点击下面的按钮,获取微信二维码!</div> <div id="expire" style="display: none;">请尽快使用手机微信扫码,二维码<span id="dd">120</span>秒后过期。</div> <a class="J_scanWeixin">获取微信二维码</a> </div> <script type="text/javascript"> var ws,ping_t,qrcode_t,expire_t; var WEB_URL = { QRCODE_IMG_URL : 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=', wshost : 'wss://www.iyuu.cn:2129', //websocket服务器地址 }; // 连接服务器 function connect() { ws = new WebSocket(WEB_URL.wshost); ws.onopen = function(e) { console.log(ws); console.log("Server onOpen",e); ping_t = setInterval(function(){ ws.send('{"cmd":"ping"}'); console.log("ping Server"); }, 30000); }; ws.onmessage = onmessage; ws.onclose = function(e) { console.log("Server onClose",e.data); //关闭定时器 if (typeof ping_t !="undefined") { clearInterval(ping_t); console.log("clearInterval ping_t"); } if (typeof qrcode_t !="undefined") { clearInterval(qrcode_t); console.log("clearInterval qrcode_t"); } }; ws.onerror = function(e) { connect(); console.log("Server onError",e.data); }; } function onmessage(e) { var timestamp = new Date().getTime(); var data = JSON.parse(e.data); //JSON.parse() 将 JSON字符串转换为对象。 if (typeof data.cmd != 'undefined') { switch(data.cmd){ case 'login': window.location='/admin/login.php?token='+data.token; break; case 'scan': console.log('Server Cmd scan',e.data); break; case 'bind': console.log('Server Cmd bind',e.data); break; default: //服务器下发其他指令 console.log('Server Cmd?',e.data); break; } }else{ if (typeof data.token != 'undefined') { clearInterval(expire_t); clearInterval(qrcode_t); $("#token").html("<h3>您的Token是:"+ data.token +"</h3><br /><h3>请求URL是:https://www.iyuu.cn/"+ data.token +".send</h3>"); $("#token").show(); $("#qrcode").hide(); $("#expire").hide(); $(".J_scanWeixin").hide(); } } console.log('收到Server消息',e.data); } connect(); //dom载入完毕执行 $(function(){ //点击按钮,显示二维码 $('.J_scanWeixin').click(function(){ if (ws.readyState == 1) { $.get("/qrcode",function(ret){ ws.send(ret); //发送uid var data = JSON.parse(ret); $("#qrcode").html("<img class='' src='"+ WEB_URL.QRCODE_IMG_URL + escape(data.ticket) +"' width='375' height='375' />"); $(".J_scanWeixin").hide(); //隐藏获取二维码按钮 $("#qrcode").show(); $("#expire").show(); //显示倒计时 //扫码提示 qrcode_t = setTimeout(function(){ $("#qrcode").hide(); $("#expire").hide(); $(".J_scanWeixin").show(); }, data.expire_seconds*1000); var dd = data.expire_seconds; expire_t = setInterval(function(){ if(dd <=1){ clearInterval(expire_t); } dd--; $("#dd").html("<b>"+ dd +"</b>"); }, 1000); }); }else{ $("#qrcode").html("<b>Websocker链接失败,请刷新页面重试!</b>"); $(".J_scanWeixin").hide(); //隐藏获取二维码按钮 } }); }); </script> </body> </html> ~~~ # 执行方法:       /磁盘/路径/php /路径/start\_wss.php start -d