1. 什么是心跳

其实简单的说就是:客户端隔一段时间就给服务端发送消息,用来告诉服务端这个连接没有断,是正常的,从而维护长连接的持久性。

如果不加心跳包,有的服务器节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉;而且这中间指不定会有什么乱七八糟的比如机器断电、网线拔出这些幺蛾子出现导致客户端断线。

但是类似断网这种极端情况导致客户端断开连接,服务端是不知道的。因为客户端在正常情况下主动断开会向服务端发送一个tcp的fin包。

什么是FIN包?
FIN包表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了。
发送FIN标志 位的TCP数据包后,连接将被断开。
这个标志的数据包也经常被用于进行端口扫描。

然而极端情况下,客户端没有机会发这个包,就会导致服务端并不知道客户端断掉了。

2. 长连接必须加心跳

长连接,只要是长连接,长时间不通讯肯定会被防火墙干掉然后断开,服务在非可控情况下断开是非常不好的情况;所以长连接无论如何都要加上心跳包。

3. 心跳实例

接下来以workerman建立websocket连接为实例。

3.1 前端JavaScript代码

在onopen连接建立的时候,定义一个定时器,每10秒钟发送一个包,包的内容随意。

var ws = new WebSocket("ws://www.goozp.com");

//连接websocket
ws.onopen = function () {
    setInterval(function () {
        ws.send('Hello!');
    }, 10000)
};

一般发送心跳包的间隔在60秒以内。

3.2 Workerman中处理断线

此处参考:官方文档:心跳

可以先定义一些常量在之后用到,方便配置:

define('HEARTBEAT_TIME', 30); // 定义一个心跳间隔30秒
define('CHECK_HEARTBEAT_TIME', 1); // 检查连接的间隔时间

当接收到信息时,我们就记录下接收到信息的时间:

$worker->onMessage = function($connection, $msg) {
    // 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
    $connection->lastMessageTime = time();

    // TODO 其它业务逻辑...
};

在进程启动后设置一个定时器,每隔一段时间遍历一遍当前worker的所有连接

// 进程启动后设置一个每秒运行一次的定时器
$worker->onWorkerStart = function($worker) {
    Timer::add(CHECK_HEARTBEAT_TIME, function()use($worker){
        $time_now = time();
        foreach($worker->connections as $connection) {
            // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
            if (empty($connection->lastMessageTime)) {
                $connection->lastMessageTime = $time_now;
                continue;
            }

            // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
            if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                $connection->close();
            }
        }
    });
};

这样,结合前端的心跳包,我们就可以做到维持连接的长久,以及踢出设置时间段内未使用的废弃连接。