上一節(jié)我們講述了websocket在swoole中的使用嚷掠,并且我們也給出了一個(gè)簡(jiǎn)單的聊天模型,不同的客戶端可以相互發(fā)消息服猪。有些同學(xué)不以為然饼拍,server有swoole提供強(qiáng)大的API,客戶端由h5提供websocket API向楼,操作很方便查吊,沒感覺到什么問題呀,這一章節(jié)是否有存在的必要性呢湖蜕?
有逻卖,非常有。今天我們就針對(duì)websocket中常見的幾個(gè)問題做一個(gè)詳細(xì)的總結(jié)說明昭抒,具體要說的重點(diǎn)大概有下面3個(gè)
- 心跳檢測(cè)的必要性
- 校驗(yàn)客戶端連接的有效性
- 客戶端的重連機(jī)制
我們分別來看下
心跳檢測(cè)
還記得我們?cè)谶M(jìn)程模型一文中介紹的Master進(jìn)程嗎评也?當(dāng)時(shí)我們說過,Master進(jìn)程灭返,包括主線程盗迟,多個(gè)Reactor線程等。其實(shí)主進(jìn)程內(nèi)還包括其他線程熙含,比如我們現(xiàn)在講的心跳檢測(cè)罚缕,在Master進(jìn)程內(nèi)就有專門用于心跳檢測(cè)的線程。
那到底什么是心跳檢測(cè)呢怎静?說著websocket怕磨,怎么談到要醫(yī)治病人了喂饥?這個(gè)心跳檢測(cè)呢,是server定時(shí)檢測(cè)客戶端是否還連接的意思肠鲫,即server定時(shí)檢測(cè)client是否還活著员帮,所以我們說的專業(yè)點(diǎn)就是所謂的心跳檢測(cè)。
等等导饲,老師你說“定時(shí)檢測(cè)”捞高?是不是說之前學(xué)的定時(shí)器可以派上用場(chǎng)了?
怎么感覺之前講的不教你在實(shí)際場(chǎng)景中運(yùn)用一次你就不會(huì)似的渣锦。當(dāng)然硝岗,你要是用定時(shí)器也沒問題,不過呢袋毙,我們都說有專門的心跳檢測(cè)線程的存在了型檀,所以,我們只需要簡(jiǎn)單的配置听盖,開啟這個(gè)心跳檢測(cè)線程就可以了胀溺。
有同學(xué)還有疑問,server我們有onClose回調(diào)皆看,客戶端斷開連接我們可以主動(dòng)關(guān)閉連接或者刪除客戶端的映射關(guān)系仓坞,再者說,即使連接無效腰吟,斷了就斷了唄无埃,反正我的server面向的client也沒有多少,心跳檢測(cè)就真的有存在的必要性么毛雇?
正常情況下嫉称,不需要×榇客戶端斷開連接能夠通知到server织阅,server自然也就可以主動(dòng)關(guān)閉連接。但是始藕,有很多非正常情況的存在蒲稳,比如斷電斷網(wǎng)尤其是移動(dòng)網(wǎng)絡(luò)盛行的當(dāng)下氮趋,二者之間建立的友好關(guān)系(連接)非常不穩(wěn)定伍派,這就必然會(huì)導(dǎo)致大量的fd(fd的數(shù)量是有限的,還記得最大是多少嗎剩胁?)被浪費(fèi)诉植!所以為了解決這些問題,swoole內(nèi)置了心跳檢測(cè)機(jī)制昵观。
我們只需要做如下簡(jiǎn)單的配置即可
$serv->set([
'heartbeat_check_interval' => N,
'heartbeat_idle_time' => M,
]);
如上晾腔,分別配置heartbeat_check_interval和heartbeat_idle_time參數(shù)舌稀,二者配合使用,其含義就是N秒檢查一次灼擂,看看哪些連接M內(nèi)沒有活動(dòng)的壁查,就認(rèn)為這個(gè)連接是無效的,server就會(huì)主動(dòng)關(guān)閉這個(gè)無效的連接剔应。
是不是說N秒server會(huì)主動(dòng)向客戶端發(fā)一個(gè)心跳包睡腿,沒有收到客戶端響應(yīng)的才認(rèn)為這個(gè)連接是死連接呢?那還要heartbeat_idle_time做什么峻贮,對(duì)吧席怪?
swoole的實(shí)現(xiàn)原理是這樣的:server每次收到客戶端的數(shù)據(jù)包都會(huì)記錄一個(gè)時(shí)間戳,N秒內(nèi)循環(huán)檢測(cè)下所有的連接纤控,如果M秒內(nèi)該連接還沒有活動(dòng)挂捻,才斷開這個(gè)連接。
心跳檢測(cè)的問題船万,記得自己動(dòng)手實(shí)踐實(shí)踐哦刻撒,有不懂的可以下面給我留言。
校驗(yàn)客戶端連接的有效性
按照我們上文創(chuàng)建的websocket server唬涧,當(dāng)然只有本地的ip才能連接上疫赎,因?yàn)閟erver監(jiān)聽的ip是127.0.0.1。實(shí)際項(xiàng)目上線后碎节,如果你的websocket server是對(duì)外開放的捧搞,就需要把ip修改為服務(wù)器外網(wǎng)的ip地址或者修改為0.0.0.0。
如此狮荔,也便帶來了新的問題:
任意客戶端都可以連接到我們的server了胎撇,這個(gè)“任意”可不止我們自己認(rèn)為有效的客戶端,還包括你的我的所有的非有效或者惡意的連接殖氏,這可不是我們想要的晚树。
如何避免這一問題呢?方法有很多種雅采,比如我們可以在連接的時(shí)候認(rèn)為只有g(shù)et傳遞的參數(shù)valid=1才允許連接爵憎;或者我們只允許登錄用戶才可以連接server;再或者我們可以校驗(yàn)客戶端每次send所攜帶的token婚瓜,server對(duì)該值校驗(yàn)通過后才認(rèn)為當(dāng)前是有效連接等等宝鼓。與此同時(shí),server開啟心跳檢測(cè)巴刻,對(duì)于惡意無效的連接愚铡,直接干掉!
上面簡(jiǎn)單的介紹了一些解決方案,下面我們以client 連接server時(shí)攜帶token為例做一個(gè)實(shí)際說明沥寥。
首先我們只允許登錄用戶才可以連接server碍舍,假設(shè)某用戶的唯一標(biāo)識(shí)uid=100,token的生成規(guī)則我們約定如下:token=md5(md5(uid)+key)邑雅,其中key=客戶端和服務(wù)端雙方約定的某個(gè)字符串片橡,我們這里假設(shè)key="^manks.top&swoole$",不包括雙引號(hào)淮野。
server的代碼實(shí)現(xiàn)如下(詳細(xì)的代碼參考WebSocketServerValid.php )
<?php
class WebSocketServerValid
{
private $_serv;
public $key = '^manks.top&swoole$';
public function __construct()
{
$this->_serv = new swoole_websocket_server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
'heartbeat_check_interval' => 30,
'heartbeat_idle_time' => 62,
]);
$this->_serv->on('open', [$this, 'onOpen']);
$this->_serv->on('message', [$this, 'onMessage']);
$this->_serv->on('close', [$this, 'onClose']);
}
/**
* @param $serv
* @param $request
*/
public function onOpen($serv, $request)
{
$this->checkAccess($serv, $request);
}
/**
* @param $serv
* @param $frame
*/
public function onMessage($serv, $frame)
{
$this->_serv->push($frame->fd, 'Server: ' . $frame->data);
}
public function onClose($serv, $fd)
{
echo "client {$fd} closed.\n";
}
/**
* 校驗(yàn)客戶端連接的合法性,無效的連接不允許連接
* @param $serv
* @param $request
* @return mixed
*/
public function checkAccess($serv, $request)
{
// get不存在或者uid和token有一項(xiàng)不存在锻全,關(guān)閉當(dāng)前連接
if (!isset($request->get) || !isset($request->get['uid']) || !isset($request->get['token'])) {
$this->_serv->close($request->fd);
return false;
}
$uid = $request->get['uid'];
$token = $request->get['token'];
// 校驗(yàn)token是否正確,無效關(guān)閉連接
if (md5(md5($uid) . $this->key) != $token) {
$this->_serv->close($request->fd);
return false;
}
}
public function start()
{
$this->_serv->start();
}
}
$server = new WebSocketServerValid;
$server->start();
可以看到,checkAccess是授權(quán)方法录煤,我們?cè)趏nOpen回調(diào)內(nèi)對(duì)uid以及token進(jìn)行了校驗(yàn)鳄厌,無效則關(guān)閉連接。
為了模擬效果妈踊,我們分別貼上兩種客戶端代碼了嚎,連接失敗和連接成功
連接失敗的主要jsdiamante如下(詳細(xì)代碼見源碼的websocket-client-faild.html)
var ws = new WebSocket('ws://127.0.0.1:9501');
ws.onopen = function(event) {
ws.send('This is websocket client.');
};
ws.onmessage = function(event) {
console.log(event.data);
};
ws.onclose = function(event) {
console.log('Client has closed.\n');
};
無論是console控制臺(tái)還是server終端我們都可以看到客戶端連接被關(guān)閉的提醒。下面我們?cè)倏茨M一種成功的結(jié)果
部分php代碼和js代碼如下(詳細(xì)代碼見源碼的websocket-client-success.html)
<?php
$key = '^manks.top&swoole$';
$uid = 100;
$token = md5(md5($uid) . $key);
?>
<script>
var ws = new WebSocket("ws://127.0.0.1:9501?uid=<?php echo $uid; ?>&token=<?php echo $token; ?>");
ws.onopen = function(event) {
ws.send('This is websocket client.');
};
ws.onmessage = function(event) {
console.log(event.data);
};
ws.onclose = function(event) {
console.log('Client has closed.\n');
};</script>
可以看到廊营,這次連接沒有被關(guān)閉且console控制臺(tái)會(huì)正常輸出一些信息
Server: This is websocket client.
即我們完成了校驗(yàn)連接有效性的案例歪泳,下面我們接著看最后一個(gè)問題
客戶端重連機(jī)制
有同學(xué)注意到,我們剛剛設(shè)置的心跳檢測(cè)時(shí)間是30秒露筒,如果客戶端62秒內(nèi)沒有與server通信呐伞,server會(huì)關(guān)閉該連接,即部分人在上述success案例中的console控制臺(tái)上會(huì)看到Client has closed.的提醒慎式。這是我們?cè)O(shè)置的機(jī)制伶氢,屬于正常現(xiàn)象瘪吏。
那我們要說的重連機(jī)制又是什么呢癣防?
客戶端重連機(jī)制又可以理解為一種保活機(jī)制掌眠,你也可以跟服務(wù)端的心跳檢測(cè)在一起理解為雙向心跳蕾盯。即我們有一種需求是,如何能保證客戶端和服務(wù)端的連接一直是有效的蓝丙,不斷開的级遭。
其實(shí)很簡(jiǎn)單,對(duì)客戶端而言渺尘,只要觸發(fā)error或者close再或者連接失敗挫鸽,就主動(dòng)重連server,這便是我們的目的沧烈。
下面貼一段js代碼掠兄,來解決這個(gè)問題(詳細(xì)代碼見commentClient.html)
<script>
var ws;//websocket實(shí)例
var lockReconnect = false;//避免重復(fù)連接
var wsUrl = 'ws://127.0.0.1:9501';
function createWebSocket(url) {
try {
ws = new WebSocket(url);
initEventHandle();
} catch (e) {
reconnect(url);
}
}
function initEventHandle() {
ws.onclose = function () {
reconnect(wsUrl);
};
ws.onerror = function () {
reconnect(wsUrl);
};
ws.onopen = function () {
//心跳檢測(cè)重置
heartCheck.reset().start();
};
ws.onmessage = function (event) {
//如果獲取到消息像云,心跳檢測(cè)重置
//拿到任何消息都說明當(dāng)前連接是正常的
heartCheck.reset().start();
}
}
function reconnect(url) {
if(lockReconnect) return;
lockReconnect = true;
//沒連接上會(huì)一直重連锌雀,設(shè)置延遲避免請(qǐng)求過多
setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 2000);
}
//心跳檢測(cè)
var heartCheck = {
timeout: 60000,//60秒
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
//這里發(fā)送一個(gè)心跳蚂夕,后端收到后,返回一個(gè)心跳消息腋逆,
//onmessage拿到返回的心跳就說明連接正常
ws.send("");
self.serverTimeoutObj = setTimeout(function(){//如果超過一定時(shí)間還沒重置婿牍,說明后端主動(dòng)斷開了
ws.close();//如果onclose會(huì)執(zhí)行reconnect,我們執(zhí)行ws.close()就行了.如果直接執(zhí)行reconnect 會(huì)觸發(fā)onclose導(dǎo)致重連兩次
}, self.timeout);
}, this.timeout);
}
}
createWebSocket(wsUrl);
</script>
在這種情況下惩歉,你可以嘗試把server中斷或者斷網(wǎng)試試等脂,結(jié)果是client會(huì)不停的每隔一定時(shí)間嘗試連接server,直至連接成功撑蚌。