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 )

_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)




可以看到,这次连接没有被关闭且console控制台会正常输出一些信息

Server: This is websocket client.

即我们完成了校验连接有效性的案例,下面我们接着看最后一个问题

客户端重连机制

有同学注意到,我们刚刚设置的心跳检测时间是30秒,如果客户端62秒内没有与server通信,server会关闭该连接,即部分人在上述success案例中的console控制台上会看到Client has closed.的提醒。这是我们设置的机制,属于正常现象。

那我们要说的重连机制又是什么呢?

客户端重连机制又可以理解为一种保活机制,你也可以跟服务端的心跳检测在一起理解为双向心跳。即我们有一种需求是,如何能保证客户端和服务端的连接一直是有效的,不断开的。

其实很简单,对客户端而言,只要触发error或者close再或者连接失败,就主动重连server,这便是我们的目的。

下面贴一段js代码,来解决这个问题(详细代码见commentClient.html)


在这种情况下,你可以尝试把server中断或者断网试试,结果是client会不停的每隔一定时间尝试连接server,直至连接成功。

你可能感兴趣的:(swoole基础-常见的websocket问题)