在正常業(yè)務(wù)中經(jīng)常會(huì)碰到服務(wù)器需要向客戶端發(fā)起主動(dòng)推送數(shù)據(jù)的需求,其實(shí)有很多實(shí)現(xiàn)方案箍鼓,
①樊销、客戶端輪詢接口
- 優(yōu)點(diǎn):簡(jiǎn)單
- 缺點(diǎn):如果是請(qǐng)求頻率比較高,業(yè)務(wù)場(chǎng)景比較常用声诸,可能會(huì)對(duì)服務(wù)器造成比較大的壓力酱讶。
②、借用第三方推送服務(wù)的靜默推送(即消息模式)彼乌,比如友盟泻肯,極光
- 優(yōu)點(diǎn):對(duì)服務(wù)器壓力小,不需要自建長(zhǎng)連接服務(wù)慰照,移動(dòng)客戶端支持也比較好
- 缺點(diǎn):依賴于第三方灶挟,受制于人,web版不支持
③毒租、自建長(zhǎng)連接服務(wù)
- 優(yōu)點(diǎn):提升技能點(diǎn)稚铣,鍛煉能力,可定制化需求墅垮,自由度高
- 缺點(diǎn):需要開(kāi)發(fā)時(shí)間惕医,配置socket服務(wù),需守護(hù)進(jìn)程保證服務(wù)不中斷
下面我們就開(kāi)始研究如何用PHP實(shí)現(xiàn)長(zhǎng)連接問(wèn)題:
PHP自身支持socket編程算色,但是比較繁瑣抬伺,網(wǎng)上常用的輪子有兩種 swoole (c 擴(kuò)展) 和 workerman(PHPsocket),本文以workerman為例灾梦。
1峡钓、下載workerman包
workerman官網(wǎng)地址:https://www.workerman.net/workerman
支持直接下載,或者composer安裝
2若河、測(cè)試socket連接
首先在把下載的包解壓放在php項(xiàng)目里能岩,在根目錄建立一個(gè)start.php文件
<?php
use Workerman\Worker;
require_once 'Autoloader.php';
// 創(chuàng)建一個(gè)Worker監(jiān)聽(tīng)2346端口,使用websocket協(xié)議通訊
$ws_worker = new Worker("websocket://0.0.0.0:2345");
// 啟動(dòng)4個(gè)進(jìn)程對(duì)外提供服務(wù)
$ws_worker->count = 4;
// 當(dāng)收到客戶端發(fā)來(lái)的數(shù)據(jù)后返回hello $data給客戶端
$ws_worker->onMessage = function($connection, $data)
{
// 向客戶端發(fā)送hello $data
$connection->send('hello ' . $data);
};
// 運(yùn)行
Worker::runAll();
然后建立html文件牡肉,index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>測(cè)試websocket</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的瀏覽器支持 WebSocket!");
// 打開(kāi)一個(gè) web socket
var ws = new WebSocket("ws://127.0.0.1:2345");
ws.onopen = function()
{
// Web Socket 已連接上捧灰,使用 send() 方法發(fā)送數(shù)據(jù)
ws.send("發(fā)送數(shù)據(jù)");
alert("數(shù)據(jù)發(fā)送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert(received_msg);
};
ws.onclose = function()
{
// 關(guān)閉 websocket
alert("連接已關(guān)閉...");
};
}
else
{
// 瀏覽器不支持 WebSocket
alert("您的瀏覽器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">運(yùn)行 WebSocket</a>
</div>
</body>
</html>
然后在cmd命令行窗口進(jìn)入項(xiàng)目目錄,運(yùn)行
php start.php start -d
會(huì)看到
就代表服務(wù)以及啟動(dòng)成功了
接下里打開(kāi)index.html统锤,運(yùn)行毛俏,如果能正常收到頁(yè)面的alert消息,就代表通訊已經(jīng)沒(méi)有問(wèn)題了饲窿。
3煌寇、正式業(yè)務(wù)中如何使用主動(dòng)推送
上面的例子,我們只是建立好了socket連接逾雄,客戶端在發(fā)送內(nèi)容到服務(wù)器之后能收到返回的消息阀溶,這時(shí)候我們?nèi)绾巫尫?wù)器主動(dòng)給客戶端推送消息呢腻脏。實(shí)現(xiàn)的思想其實(shí)是建立一個(gè)對(duì)外監(jiān)聽(tīng)的worker容器,再開(kāi)啟一個(gè)內(nèi)部數(shù)據(jù)推送監(jiān)聽(tīng)的端口银锻,再把客戶端通過(guò)uid做一個(gè)映射永品,通過(guò)監(jiān)聽(tīng)內(nèi)部端口的數(shù)據(jù),來(lái)實(shí)現(xiàn)把數(shù)據(jù)轉(zhuǎn)發(fā)到對(duì)應(yīng)的映射內(nèi)的客戶端來(lái)實(shí)現(xiàn)击纬。友盟的推送鼎姐,laravel的廣播功能,都是通過(guò)這種邏輯實(shí)現(xiàn)的更振。
下面分別貼一下服務(wù)器端服務(wù)代碼炕桨,服務(wù)器端推送代碼,客戶端html代碼就可以輕松看明白了肯腕。
a献宫、服務(wù)代碼 start.php
<?php
use Workerman\Worker;
require_once 'Autoloader.php';
// 初始化一個(gè)worker容器,監(jiān)聽(tīng)1234端口
global $worker;
$worker = new Worker('websocket://0.0.0.0:1234');
// 這里進(jìn)程數(shù)必須設(shè)置為1
$worker->count = 1;
// worker進(jìn)程啟動(dòng)后建立一個(gè)內(nèi)部通訊端口
$worker->onWorkerStart = function($worker)
{
// 開(kāi)啟一個(gè)內(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;
// $data數(shù)組格式,里面有uid奈惑,表示向那個(gè)uid的頁(yè)面推送數(shù)據(jù)
$data = json_decode($buffer, true);
$uid = $data['uid'];
// 通過(guò)workerman吭净,向uid的頁(yè)面推送數(shù)據(jù)
$ret = sendMessageByUid($uid, $buffer);
// 返回推送結(jié)果
$connection->send($ret ? 'ok' : 'fail');
};
$inner_text_worker->listen();
};
// 新增加一個(gè)屬性,用來(lái)保存uid到connection的映射
$worker->uidConnections = array();
// 當(dāng)有客戶端發(fā)來(lái)消息時(shí)執(zhí)行的回調(diào)函數(shù)
$worker->onMessage = function($connection, $data)use($worker)
{
// 判斷當(dāng)前客戶端是否已經(jīng)驗(yàn)證,既是否設(shè)置了uid
if(!isset($connection->uid))
{
// 沒(méi)驗(yàn)證的話把第一個(gè)包當(dāng)做uid(這里為了方便演示肴甸,沒(méi)做真正的驗(yàn)證)
$connection->uid = $data;
/* 保存uid到connection的映射寂殉,這樣可以方便的通過(guò)uid查找connection,
* 實(shí)現(xiàn)針對(duì)特定uid推送數(shù)據(jù)
*/
$worker->uidConnections[$connection->uid] = $connection;
$connection->send($data);
return;
}
};
// 當(dāng)有客戶端連接斷開(kāi)時(shí)
$worker->onClose = function($connection)use($worker)
{
global $worker;
if(isset($connection->uid))
{
// 連接斷開(kāi)時(shí)刪除映射
unset($worker->uidConnections[$connection->uid]);
}
};
// 向所有驗(yàn)證的用戶推送數(shù)據(jù)
function broadcast($message)
{
global $worker;
foreach($worker->uidConnections as $connection)
{
$connection->send($message);
}
}
// 針對(duì)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;
}
// 運(yùn)行所有的worker(其實(shí)當(dāng)前只定義了一個(gè))
Worker::runAll();
b原在、推送代碼 push.php
<?php
// 建立socket連接到內(nèi)部推送端口
$client = stream_socket_client('tcp://127.0.0.1:5678', $errno, $errmsg, 1);
// 推送的數(shù)據(jù)友扰,包含uid字段,表示是給這個(gè)uid推送庶柿,這里可以通過(guò)修改uid來(lái)測(cè)試給哪個(gè)客戶端發(fā)推送
$data = array('uid'=>'uid4', 'percent'=>'88%');
// 發(fā)送數(shù)據(jù)村怪,注意5678端口是Text協(xié)議的端口,Text協(xié)議需要在數(shù)據(jù)末尾加上換行符
fwrite($client, json_encode($data)."\n");
// 讀取推送結(jié)果
echo fread($client, 8192);
c浮庐、客戶端HTML index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>uid4的接收頁(yè)面(修改uid即可測(cè)試給哪個(gè)客戶端推送)</title>
<script type="text/javascript">
var ws = new WebSocket('ws://127.0.0.1:1234');
ws.onopen = function(){
var uid = 'uid4';
ws.send(uid);
};
ws.onmessage = function(e){
alert(e.data);
};
</script>
</head>
<body>
<div id="so">
測(cè)試頁(yè)面
</div>
</body>
</html>
先啟動(dòng)服務(wù)甚负,再打開(kāi)網(wǎng)頁(yè),最后運(yùn)行push.php即可測(cè)試审残,這里比較關(guān)鍵的是進(jìn)程數(shù)必須設(shè)置為1梭域,否則可能無(wú)法推送成功。一個(gè)基礎(chǔ)的長(zhǎng)連接推送就這樣ok了搅轿。
如果需要多進(jìn)程啦病涨、服務(wù)器集群啦、就需要基于Channel組件或者GatewayWorker了璧坟,更多進(jìn)階功能可以參考官方文檔http://doc.workerman.net
wss的nginx服務(wù)器配置
話不多說(shuō)粘貼配置,這個(gè)放在https的配置里面
location /wss
{
proxy_pass http://127.0.0.1:2345;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
rewrite /wss/(.*) /$1 break;
proxy_redirect off;
}
接下來(lái)吧前端頁(yè)面的代碼做修改
var ws = new WebSocket("wss://api.pinkechuxing.com/wss");
就是這么簡(jiǎn)單就配置好了
在實(shí)際的使用中我們可能會(huì)遇到連接中斷的情況,這個(gè)時(shí)候就需要發(fā)送心跳包來(lái)維持連接
var ws = new WebSocket("ws://www.goozp.com");
//連接websocket
ws.onopen = function () {
setInterval(function () {
ws.send('Hello!');
}, 10000)
};