本文是我在實際工作中用到的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服務器端
<?php
require_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();