查看源代碼,有完整的通訊流程實現(xiàn)。
https://iyuu.cn
發(fā)表在自己的博客
https://www.iyuu.cn/archives/202/
?#說明
此功能是利用微信公眾號帶參數(shù)二維碼,實現(xiàn)掃碼識別用戶,并且實時通知前端掃碼狀態(tài),并非ajax輪詢!從而進(jìn)行后續(xù)的其他業(yè)務(wù)邏輯。
?1. 用workerman框架,編寫websocket服務(wù)后端監(jiān)聽2129端口
,進(jìn)程啟動同時再監(jiān)聽一個內(nèi)部通訊5678端口
,2129端口
等待前端頁面發(fā)起連接:https://www.iyuu.cn/usr/index.html
;
?2. 用戶進(jìn)入前端頁面,自動連接wss://www.iyuu.cn:2129
;
?3. 用戶點擊獲取二維碼
,請求二維碼生成接口:https://www.iyuu.cn/qrcode
,返回二維碼參數(shù):
? json {"ticket":"gQH47zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycTMtdzlMVEhlYzIxcF9jQU50MWsAAgQHjGRdAwR4AAAA","expire_seconds":120,"uid":1735536450} ?
? 注:uid通過函數(shù)rand(1,4294967200)
生成并查詢緩存,確保唯一后放入Redis緩存。
?4. 把二維碼參數(shù)
,轉(zhuǎn)發(fā)到websocket服務(wù)wss://www.iyuu.cn:2129
,websocket服務(wù)保存轉(zhuǎn)發(fā)來的信息建立映射關(guān)系;
?5. 顯示二維碼:https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={data.ticket},用戶掃碼;
?6. 微信開發(fā)者接口會收到掃碼結(jié)果,獲取到場景值ID;
?7. 根據(jù)場景值ID從Redis緩存取出ticket校驗通過,執(zhí)行業(yè)務(wù)邏輯(登錄、綁定、解綁、積分等等),并通過5678端口
實時通知用戶掃碼后的處理結(jié)果。
<?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';
/*
* 注意這里進(jìn)程數(shù)必須設(shè)置為1,否則會報端口占用錯誤
* (php 7可以設(shè)置進(jìn)程數(shù)大于1,前提是$inner_text_worker->reusePort=true)
*/
$worker->count = 1;
// 新增加一個屬性,用來保存uid到connection的映射(uid是用戶id或者客戶端唯一標(biāo)識)
$worker->uidConnections = array();
// 當(dāng)有客戶端連接時
$worker->onConnect = function($connection)
{
/*
//定時10秒關(guān)閉這個鏈接,需要10秒內(nèi)發(fā)認(rèn)證并刪除定時器阻止關(guān)閉連接的執(zhí)行
$connection->auth_timer_id = Timer::add(10, function(){
$connection->close();
});
Timer::del($connection->auth_timer_id);
*/
};
// worker進(jìn)程啟動后創(chuàng)建一個text Worker以便打開一個內(nèi)部通訊端口
$worker->onWorkerStart = function($worker)
{
sc('WebSocket服務(wù)進(jìn)程啟動成功!');
// 開啟一個內(nèi)部端口,方便內(nèi)部系統(tǒng)推送數(shù)據(jù),Text協(xié)議格式 文本+換行符
$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數(shù)組格式,里面有uid,表示向那個uid的頁面推送數(shù)據(jù)
$data = json_decode($buffer, true);
if (isset($data['uid'])) {
$uid = $data['uid'];
//uid + ticket雙重安全驗證(防止前端冒用隨機(jī)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的頁面推送數(shù)據(jù)
$ret = sendMessageByUid($uid, $buffer);
// 返回推送結(jié)果
$connection->send($ret ? 'ok' : 'fail');
}
return;
};
// ## 執(zhí)行監(jiān)聽 ##
$inner_text_worker->listen();
// 進(jìn)程啟動后設(shè)置一個每秒運行一次的定時器
Timer::add(1, function()use($worker){
$time_now = time();
foreach($worker->uidConnections as $connection) {
// 有可能該connection還沒收到過消息,則lastMessageTime設(shè)置為當(dāng)前時間
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通訊時間間隔大于心跳間隔,則認(rèn)為客戶端已經(jīng)下線,關(guān)閉連接
if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
if(isset($connection->uid))
{
// 連接斷開時刪除映射
unset($worker->uidConnections[$connection->uid]);
}
$connection->close();
}
}
});
//每天重啟進(jìn)程
Timer::add(86400, function()use($worker)
{
sc('WebSocket服務(wù)進(jìn)程定時重啟任務(wù),執(zhí)行成功!');
Worker::stopAll();
});
};
// 當(dāng)有客戶端發(fā)來消息時執(zhí)行的回調(diào)函數(shù)
$worker->onMessage = function($connection, $data)
{
global $worker;
// 給connection臨時設(shè)置一個lastMessageTime屬性,用來記錄上次收到消息的時間
$connection->lastMessageTime = time();
// 客戶端傳遞的是json數(shù)據(jù)
if (empty($data)) return;
$message = json_decode($data, true);
if(empty($message)) return;
if(isset($message['cmd'])) {
// 根據(jù)類型執(zhí)行不同的業(yè)務(wù)
switch($message['cmd'])
{
case 'ping':
return;
case 'login':
return;
case 'sms':
return;
case 'mail':
return;
default:
return;
}
}else{
// 判斷當(dāng)前客戶端是否已經(jīng)驗證,即是否設(shè)置了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']) {
// 沒驗證的話把第一個包當(dāng)做uid
$connection->uid = $message['uid'];
if (isset($message['ticket'])) {
//帶參數(shù)二維碼的ticket
$connection->ticket = $message['ticket'];
}
/* 保存uid到connection的映射,這樣可以方便的通過uid查找connection,
* 實現(xiàn)針對特定uid推送數(shù)據(jù)
*/
$worker->uidConnections[$connection->uid] = $connection;
$connection->send($data);
return;
} else {
//不帶uid的消息
# code...
}
}
};
// 當(dāng)有客戶端連接斷開時
$worker->onClose = function($connection)
{
global $worker;
if(isset($connection->uid))
{
// 連接斷開時刪除映射
unset($worker->uidConnections[$connection->uid]);
}
};
// 進(jìn)程關(guān)閉時
$worker->onWorkerStop = function($worker)
{
//通知運維人員
//sc('WebSocket服務(wù)進(jìn)程退出,如非定時重啟,請檢查!');
};
// 針對uid推送數(shù)據(jù)
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();
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>微信公眾號模板消息通知Token申請頁 - 大衛(wèi)科技blog www.iyuu.cn</title>
<meta name="keywords" content="大衛(wèi)科技blog,www.iyuu.cn" />
<meta name="description" content="微信公眾號模板消息通知Token申請頁" />
<meta name="copyright" content="海南大衛(wèi)電子科技有限公司" />
<meta name="author" content="大衛(wèi)" />
<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 >大衛(wèi)科技blog</a></address>
</div>
<div id="token" style="display: none;"></div>
<div id="qrcode">點擊下面的按鈕,獲取微信二維碼!</div>
<div id="expire" style="display: none;">請盡快使用手機(jī)微信掃碼,二維碼<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服務(wù)器地址
};
// 連接服務(wù)器
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);
//關(guān)閉定時器
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字符串轉(zhuǎn)換為對象。
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: //服務(wù)器下發(fā)其他指令
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載入完畢執(zhí)行
$(function(){
//點擊按鈕,顯示二維碼
$('.J_scanWeixin').click(function(){
if (ws.readyState == 1)
{
$.get("/qrcode",function(ret){
ws.send(ret); //發(fā)送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
先給博主贊一個,寫得很詳細(xì),不過要是基礎(chǔ)差點的朋友可能實踐起來有點難,建議博主可以直接在第三方websocket推送框架上直接進(jìn)行開發(fā),那樣感覺會更方便些,推薦GoEasy,我一般都會用,因為它提供完整的websocket前后端解決方案,支持多種前后端語言的使用,所以哪怕只做前端或者只做后端的朋友也可以用,www.goeasy.io 有興趣可以試一下怎么樣
mark,以后應(yīng)該會有用