本文是我在实际工作中用到的Socket通信,关于心跳机制的维护方式,特意总结了一下,希望对朋友们有所帮助。
Socket应用:首先Socket 封装了tcp协议的,通过长连接的方式来与服务器通信,是由服务器和客户端两部分组成的,当客户端成功连接之后,服务器会记录这个用户,并为它分配资源,当客户端断开连接后,服务器会自动释放资源。
但在实际的网络环境中会有很多因素的导致服务器不知道客户端断开,或者客户端不知道服务器宕机,等等,比如网络中使用了路由器、交换机等等;这就带来一个问题:此时此刻服务器如何知道能否同客户端正常通信?解决这个问题的办法就是采用心跳。简单的说就是:在客户端和服务器连接成功后,隔一段时间服务器询问一下客户端是否还在,客户端收到后应答服务器"我还在",如果服务器超出一定时间(一般40-50秒)未收到客户端的应答,就判定它已经无法通信了,这时候需要释放资源,断开这个客户端用户。
客户端JS代码:
<!DOCTYPE html>
<html>
 
<head lang="en">
    <meta charset="utf-8">
    <title></title>
</head>
 
<body>
    <h3>WebSocket协议的客户端程序</h3>
    <button id="btConnect">连接到WS服务器</button>
    <button id="btSendAndReceive">向WS服务器发消息并接收消息</button>
    <button id="btClose">断开与WS服务器的连接</button>
    <div id="val"></div>
<script type="text/javascript">
    var wsClient=null;
    var lastHealthTime = 0; //记录最近一次心跳更新时间
    var heartbeatTimer = null;//心跳执行timer
    var checkTime = 10000; //心跳检查时间间隔-毫秒 10秒
    var healthTimeOut = 20000;//心跳时间间隔-毫秒  20秒
    var reconnectTimer = null;//重连接timer
    var reconnectTime = 10000;//重连接时间10秒后
    var uid = "20";
    var connectStatus = 3; //状态
    function connect(){
        if (connectStatus == 3){
            wsClient=new WebSocket('ws://127.0.0.1:8000'); //这个端口号和容器监听的端口号一致
            console.log("连接中...");
            console.log("readyState:"+wsClient.readyState);
            if (reconnectTimer){
                clearTimeout(reconnectTimer);
            }
            //连接成功
            wsClient.onopen = function(){
                connectStatus = wsClient.readyState;
                // 表名自己是uid1
                var data = uid; //1标识连接
                wsClient.send(data);
                console.log('ws客户端已经成功连接到服务器上');
                msg.innerHTML="连接成功...";
                console.log("readyState:"+wsClient.readyState);
                var time = new Date();
                lastHealthTime = time.getTime();
                if(heartbeatTimer){
                   clearInterval(heartbeatTimer);
                }
                heartbeatTimer = setInterval(function(){keepalive(wsClient)}, checkTime);
            };
            //收到消息
            wsClient.onmessage = function(e){
                console.log('ws客户端收到一个服务器消息:'+e.data);
                console.log("readyState:"+wsClient.readyState);
                val.innerHTML=e.data;
                var data = e.data;
                if (data){
                    var msg_type = data.substr(0,1);
                    var uid = data.substr(1);var time = new Date();
                    lastHealthTime = time.getTime();//更新客户端的最后一次心跳时间
                }
            }
            //错误
            wsClient.onerror = function(e){
                connectStatus = wsClient.readyState;
                console.log("error");
                console.log("readyState:"+wsClient.readyState);
                msg.innerHTML="连接错误...";
            };
            //关闭
            wsClient.onclose = function(){
                connectStatus = wsClient.readyState;
                console.log('到服务器的连接已经断开');
                msg.innerHTML="连接断开...";
                console.log("readyState:"+wsClient.readyState);
                //n秒后重连接
                reconnectTimer = setTimeout(function(){
                    connect();
                },reconnectTime);
            }
        }
    }
    
    btConnect.onclick = function(){
        connect();
    }
    btSendAndReceive.onclick = function(){
        wsClient.send('Hello Server');
    }
    btClose.onclick = function(){
        console.log("断开连接");
        console.log(wsClient.readyState);
        wsClient.close();
    }
    function keepalive(ws){
        var time = new Date();
        console.log(time.getTime()-lastHealthTime);
        if ((time.getTime()-lastHealthTime)>healthTimeOut){
            msg.innerHTML="心跳超时,请连接断开...";
            if (heartbeatTimer){
                clearInterval(heartbeatTimer);
                //n秒后重连接
                ws.close();
                reconnectTimer = setTimeout(function(){
                    connect();
                },reconnectTime);
            }
        }
        else{
            msg.innerHTML="我依然在连接状态";
            ws.send(data);
        }
    }
</script>
<div id="msg"></div>
</body>
</html>
服务端代码:
这里我采用的是PHP语言,使用workman来实现的socket服务器端
<?phprequire_once __DIR__ .'/Autoloader.php';  
use Workerman\Worker;
use Workerman\Lib\Timer;
define('HEARTBEAT_TIME', 40);//心跳间隔时间
define('CHECK_HEARTBEAT_TIME', 10); // 检查连接的间隔时间
// 初始化一个worker容器,监听1234端口
$worker = new Worker('websocket://0.0.0.0:8000');
// 这里进程数必须设置为1
$worker->count = 1;
// 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();
            }
        }
    });
};
// 新增加一个属性,用来保存uid到connection的映射
$worker->uidConnections = array();
// 当有客户端发来消息时执行的回调函数
$worker->onMessage = function($connection, $data)use($worker)
{
     $uid = $data; //uid
     //echo 'connection...'.$uid.'\n';
    // 判断当前客户端是否已经验证,既是否设置了uid
    if(!isset($connection->uid))
    {
        if (intval($msg_type) === 1){ //连接
            //上次收到的心跳消息时间
            $connection->lastMessageTime = time();
            // 没验证的话把第一个包当做uid(这里为了方便演示,没做真正的验证)
            
            $connection->uid = $uid;
            /* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
             * 实现针对特定uid推送数据
             */
            $worker->uidConnections[$connection->uid] = $connection;
            echo  'MSG USER COUNT:'.count($worker->uidConnections); 
            echo '\n';
            return;
        }
    }
    else{
        if ($connection->uid === $uid){
          //服务器收到心跳
                //echo 'U-heart:'.$connection->uid.'\n';
                $connection->lastMessageTime = time();
                echo 'back send:';
                $buffer = $uid;
                $ret =  sendMessageByUid($uid, $buffer);
                $result = $ret ? 'ok' : 'fail';
              //  echo $result;
        }
    }
};
// 当有客户端连接断开时
$worker->onClose = function($connection)use($worker)
{
    global $worker;
    if(isset($connection->uid))
    {
        // 连接断开时删除映射
        unset($worker->uidConnections[$connection->uid]);
        echo  'CLOSE USER COUNT:'.count($worker->uidConnections); 
        echo '-'.$connection->uid.' closed';
        echo '\n';
    }
};
// 向所有验证的用户推送数据
function broadcast($message)
{
    global $worker;
    foreach($worker->uidConnections as $connection)
    {
        $connection->send($message);
    }
}
// 针对uid推送数据
function sendMessageByUid($uid, $message)
{
    global $worker;
    if(isset($worker->uidConnections[$uid]))
    {
        $connection = $worker->uidConnections[$uid];
        $connection->send($message);
        return true;
    }
    return false;
}
// 运行所有的worker(其实当前只定义了一个)
Worker::runAll();
 
  
  
  
 
 
 