18. swoole基礎-常見的websocket問題


上一節我們講述了websocket在swoole中的使用,並且我們也給出了一個簡單的聊天模型,不同的客戶端可以相互發消息。有些同學不以為然,server有swoole提供強大的API,客戶端由h5提供websocket API,操作很方便,沒感覺到什么問題呀,這一章節是否有存在的必要性呢?

有,非常有。今天我們就針對websocket中常見的幾個問題做一個詳細的總結說明,具體要說的重點大概有下面3個

  • 心跳檢測的必要性
  • 校驗客戶端連接的有效性
  • 客戶端的重連機制

我們分別來看下

心跳檢測

還記得我們在進程模型一文中介紹的Master進程嗎?當時我們說過,Master進程,包括主線程,多個Reactor線程等。其實主進程內還包括其他線程,比如我們現在講的心跳檢測,在Master進程內就有專門用於心跳檢測的線程。

那到底什么是心跳檢測呢?說着websocket,怎么談到要醫治病人了?這個心跳檢測呢,是server定時檢測客戶端是否還連接的意思,即server定時檢測client是否還活着,所以我們說的專業點就是所謂的心跳檢測。

等等,老師你說“定時檢測”?是不是說之前學的定時器可以派上用場了?

怎么感覺之前講的不教你在實際場景中運用一次你就不會似的。當然,你要是用定時器也沒問題,不過呢,我們都說有專門的心跳檢測線程的存在了,所以,我們只需要簡單的配置,開啟這個心跳檢測線程就可以了。

有同學還有疑問,server我們有onClose回調,客戶端斷開連接我們可以主動關閉連接或者刪除客戶端的映射關系,再者說,即使連接無效,斷了就斷了唄,反正我的server面向的client也沒有多少,心跳檢測就真的有存在的必要性么?

正常情況下,不需要。客戶端斷開連接能夠通知到server,server自然也就可以主動關閉連接。但是,有很多非正常情況的存在,比如斷電斷網尤其是移動網絡盛行的當下,二者之間建立的友好關系(連接)非常不穩定,這就必然會導致大量的fd(fd的數量是有限的,還記得最大是多少嗎?)被浪費!所以為了解決這些問題,swoole內置了心跳檢測機制。

我們只需要做如下簡單的配置即可

$serv->set([
    'heartbeat_check_interval' => N,
    'heartbeat_idle_time' => M,
]);

如上,分別配置heartbeat_check_interval和heartbeat_idle_time參數,二者配合使用,其含義就是N秒檢查一次,看看哪些連接M內沒有活動的,就認為這個連接是無效的,server就會主動關閉這個無效的連接。

是不是說N秒server會主動向客戶端發一個心跳包,沒有收到客戶端響應的才認為這個連接是死連接呢?那還要heartbeat_idle_time做什么,對吧?

swoole的實現原理是這樣的:server每次收到客戶端的數據包都會記錄一個時間戳,N秒內循環檢測下所有的連接,如果M秒內該連接還沒有活動,才斷開這個連接。

心跳檢測的問題,記得自己動手實踐實踐哦,有不懂的可以下面給我留言。

校驗客戶端連接的有效性

按照我們上文創建的websocket server,當然只有本地的ip才能連接上,因為server監聽的ip是127.0.0.1。實際項目上線后,如果你的websocket server是對外開放的,就需要把ip修改為服務器外網的ip地址或者修改為0.0.0.0。

如此,也便帶來了新的問題:

任意客戶端都可以連接到我們的server了,這個“任意”可不止我們自己認為有效的客戶端,還包括你的我的所有的非有效或者惡意的連接,這可不是我們想要的。

如何避免這一問題呢?方法有很多種,比如我們可以在連接的時候認為只有get傳遞的參數valid=1才允許連接;或者我們只允許登錄用戶才可以連接server;再或者我們可以校驗客戶端每次send所攜帶的token,server對該值校驗通過后才認為當前是有效連接等等。與此同時,server開啟心跳檢測,對於惡意無效的連接,直接干掉!

上面簡單的介紹了一些解決方案,下面我們以client 連接server時攜帶token為例做一個實際說明。

首先我們只允許登錄用戶才可以連接server,假設某用戶的唯一標識uid=100,token的生成規則我們約定如下:token=md5(md5(uid)+key),其中key=客戶端和服務端雙方約定的某個字符串,我們這里假設key="^manks.top&swoole$",不包括雙引號。

server的代碼實現如下(詳細的代碼參考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";
    }

    /**
     * 校驗客戶端連接的合法性,無效的連接不允許連接
     * @param $serv
     * @param $request
     * @return mixed
     */
    public function checkAccess($serv, $request)
    {
        // get不存在或者uid和token有一項不存在,關閉當前連接
        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'];
        // 校驗token是否正確,無效關閉連接
        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是授權方法,我們在onOpen回調內對uid以及token進行了校驗,無效則關閉連接。

為了模擬效果,我們分別貼上兩種客戶端代碼,連接失敗和連接成功

連接失敗的主要jsdiamante如下(詳細代碼見源碼的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控制台還是server終端我們都可以看到客戶端連接被關閉的提醒。下面我們再看模擬一種成功的結果

部分php代碼和js代碼如下(詳細代碼見源碼的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>

可以看到,這次連接沒有被關閉且console控制台會正常輸出一些信息

Server: This is websocket client.

即我們完成了校驗連接有效性的案例,下面我們接着看最后一個問題

客戶端重連機制

有同學注意到,我們剛剛設置的心跳檢測時間是30秒,如果客戶端62秒內沒有與server通信,server會關閉該連接,即部分人在上述success案例中的console控制台上會看到Client has closed.的提醒。這是我們設置的機制,屬於正常現象。

那我們要說的重連機制又是什么呢?

客戶端重連機制又可以理解為一種保活機制,你也可以跟服務端的心跳檢測在一起理解為雙向心跳。即我們有一種需求是,如何能保證客戶端和服務端的連接一直是有效的,不斷開的。

其實很簡單,對客戶端而言,只要觸發error或者close再或者連接失敗,就主動重連server,這便是我們的目的。

下面貼一段js代碼,來解決這個問題(詳細代碼見commentClient.html)

<script>
var ws;//websocket實例
var lockReconnect = false;//避免重復連接
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 () {
        //心跳檢測重置
        heartCheck.reset().start();
    };
    ws.onmessage = function (event) {
        //如果獲取到消息,心跳檢測重置
        //拿到任何消息都說明當前連接是正常的
        heartCheck.reset().start();
    }
}

function reconnect(url) {
    if(lockReconnect) return;
    lockReconnect = true;
    //沒連接上會一直重連,設置延遲避免請求過多
    setTimeout(function () {
        createWebSocket(url);
        lockReconnect = false;
    }, 2000);
}

//心跳檢測
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(){
            //這里發送一個心跳,后端收到后,返回一個心跳消息,
            //onmessage拿到返回的心跳就說明連接正常
            ws.send("");
            self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明后端主動斷開了
                ws.close();//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次
            }, self.timeout);
        }, this.timeout);
    }
}

createWebSocket(wsUrl);

</script>

在這種情況下,你可以嘗試把server中斷或者斷網試試,結果是client會不停的每隔一定時間嘗試連接server,直至連接成功。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM