前言:
心跳:心脏还在跳动,说明还有生命迹象,还活着,还活着就表示还可以继续工作,生命不止,工作不息。
WHY
为什么需要心跳检测?这个小孩没娘,说来话长,long long ago ,TCP协议的诞生,惊天地泣鬼神,改变了世界。
建立连接:三次握手
断开连接:四次挥手 ?♂?
趣解:
寓言两则:(client、server的精彩对话)。此故事纯属虚构,如有雷同,不好意思。
人物一:小C(client 客户端)
人物二:大S (server 服务端)
场景一:初识(建立连接 ——三次握手)
小C:很高兴认识你,我想和你处对象。
大S:看你挺有诚意的,我同意
小C:收到对方的回信,欣喜若狂,赶紧再说:“你等着我啊,马上咱们就*****(美好的生活)”;
(第一次握手:客户端发送信号,我准备发送数据,客户端进入准备发送状态)
(第二次握手:服务端收到信号,并给客户端回信,服务端进入准备接收状态)
(第三次握手:客户端收到回信,进入已连接状态,并给服务端确认,服务端收到,两端都进入建立连接状态)
场景二:依依惜别 (断开连接——四次挥手)
小C:我不想和你说话了,
大S:你是在开玩笑吧,
大S:(看到小C认真的面容,知道这不是玩笑),好吧
小C:你同意就好,再见。(看这大S伤心而去,驻足一段时间,自己也离开了)
(第一次挥手:客户端发出关闭请求,此时不在发送数据。)
(第二次挥手:服务端收到请求并确认,服务端进入等待关闭状态——把自己想说的话,说完)
(第三次挥手:服务端向客户端发送确认关闭信号,此时不再发送数据,进入最后确认关闭状态)
(第四次挥手:客户端收到关闭请求,向服务端恢复信息,服务端关闭。客户端会在稍后关闭)
详情查看:https://blog.csdn.net/qq_37837134/article/details/79738329
上面简单了解了TCP协议,那么有没有这样一种场景呢:只建立连接,而不断开连接
答案是肯定的,在复杂的网络环境中,这种情况必然存在。例如:夏天用电高峰期负载高,断电了;我的家里刚通网,还是不很好,时常在高潮的时候莫名其妙的断网……
总之,无缘无故的不辞而别,总是存在的,没有那么完美的爱情。
what
心跳机制:客户端定时发送 ping,服务端回复 pong。
ping、 pong 无特殊意义,就是简单的通讯传输,朋友之间常联络,才不会生疏。时间久了,服务端就要把你踢了,人的精力总是有限的,有的时候,整理自己的心情,会删除一些无用的通讯录。
服务端的资源也是有限的,那么就要把没用的fd 收回,重复利用。
fd 是什么?
fd (file descriptor) 文件描述符
一切皆文件,fd 就相当于索引,拿着这个标示去进行一系列操作。
swoole 中
- $fd是TCP客户端连接的标识符,在Server实例中是唯一的,在多个进程内不会重复
- fd 是一个自增数字,范围是1 ~ 1600万,fd超过1600万后会自动从1开始进行复用
- $fd是复用的,当连接关闭后fd会被新进入的连接复用
- 正在维持的TCP连接fd不会被复用
不要担心,同一台服务1600万的fd 不够自己用,因为服务器承受不了1600万的连接,如果真有那么多连接,肯定也不是单机了。
How
swoole 实际上已经实现了心跳检测机制,咱们只需要去开启配置就行了。如此方便,不得不赞,峰哥威武!
heartbeat_check_interval心跳检测 每隔多少秒,遍历一遍所有的连接heartbeat_idle_time心跳检测 最大闲置时间,超时触发close并关闭 默认为heartbeat_check_interval的2倍,两倍是容错机制,多一点是网络延迟的弥补
官方文档:https://wiki.swoole.com/wiki/page/283.html
当然,心跳机制也可以自己实现,定期轮询fd,是否在线,记录最近回话时间,剔除超时的连接。
伪代码
webSocket Server:
<?php
/*** 心跳检测机制 测试* Created by PhpStorm.* User: 奔跑吧笨笨* Date: 2019/8/1* Time: 3:04 PM*/include_once './class/UsersBind.php';//创建websocket服务器对象,监听0.0.0.0:9876端口
$ws = new swoole_websocket_server("0.0.0.0", 9876);$ws->set(['heartbeat_check_interval' => 30, //心跳检测 每隔多少秒,遍历一遍所有的连接'heartbeat_idle_time' => 65, //心跳检测 最大闲置时间,超时触发close并关闭 默认为heartbeat_check_interval的2倍,两倍是容错机制,多一点是网络延迟的弥补
]);//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {var_dump($request->fd, $request->get, $request->server);$ws->push($request->fd, "hello, welcome\n");//一、建立连接 uid、fd 关系绑定$bindObj = new UsersBind();$bindObj->userIdBind(1,$request->fd);$bindObj->fdBind(1,$request->fd);
});//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {//二、连接关闭 通过uid,获取fd$bindObj = new UsersBind();$fd = $bindObj->getBindFd(2);echo "Message: {$frame->data}\n";$ws->push($frame->fd, "server: {$frame->data}");
});//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {$close_str = "client-{$fd} is closed\n";echo $close_str;file_put_contents('./a.log',$close_str,FILE_APPEND);//三、连接关闭 uid、fd 关系解除$bindObj = new UsersBind();$bindObj->unbindFd(1,$fd);
});$ws->start();
客户端 client js:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>ws连接</title>
</head>
<body></body>
<script>var wsServer = 'ws://127.0.0.1:9876';var websocket = new WebSocket(wsServer);websocket.onopen = function (evt) {console.log("Connected to WebSocket server.");};websocket.onclose = function (evt) {console.log("Disconnected");};websocket.onmessage = function (evt) {console.log('Retrieved data from server: ' + evt.data);//websocket.send('Lalal');};websocket.onerror = function (evt, e) {console.log('Error occured: ' + evt.data);};
</script>
</html>
fd 结合业务user id 用户关系映射:
场景:
- 多台服务器
- 同一用户多个客户端可同时登录,消息共享
<?php/*** Created by PhpStorm.* User: runBaby* Date: 2019/8/3* Time: 5:55 PM*/
class UsersBind
{private $redisObj;private $fieldPrefix;private $serverNode;private $redisTable;private $clientNode = ['APP', 'PC'];const REDIS_HOST = '127.0.0.1';const REDIS_PORT = 6379;const REDIS_TIMEOUT = 2;public function __construct($redisTable = 'ws', $fieldPrefix = 'bind', $serverNode = 0, $client = 0){$this->redisObj = new Redis();$this->redisObj->connect(self::REDIS_HOST, self::REDIS_PORT, self::REDIS_TIMEOUT);$this->redisTable = $redisTable;$this->serverNode = $serverNode;$this->clientNode = $this->clientNode[$client];$this->fieldPrefix = $fieldPrefix;}/*** Explain: set field prefix* User: runBaby* Date: 2019/8/3* Time: 6:15 PM* @return string*/private function setFieldPrefix($type = 0){$keyPrefix = $this->fieldPrefix . ':server:' . $this->serverNode . ':client:' . $this->clientNode . ':';if ($type === 1) {$keyPrefix .= 'fd:';} else {$keyPrefix .= 'uid:';}return $keyPrefix;}/*** Explain: redis key* @param $uid* User: runBaby* Date: 2019/8/3* Time: 6:26 PM* @return string*/private function setRedisField($id, $type = 0){$fieldPrefix = $this->setFieldPrefix($type);$key = $fieldPrefix . $id;return $key;}/*** Explain: user id bind fd* @param $uid* @param $fd* User: runBaby* Date: 2019/8/3* Time: 6:23 PM* @return int*/public function userIdBind($uid, $fd){$field = $this->setRedisField($uid);$result = $this->redisObj->hSet($this->redisTable, $field, $fd);return $result;}/*** Explain: fd bind user id* @param $uid* @param $fd* User: runBaby* Date: 2019/8/4* Time: 9:08 PM* @return int*/public function fdBind($uid, $fd){$field = $this->setRedisField($fd, 1);$result = $this->redisObj->hSet($this->redisTable, $field, $uid);return $result;}/*** Explain: Two-way binding* @param $uid* @param $fd* User: runBaby* Date: 2019/8/4* Time: 9:32 PM* @return bool*/public function setBindId($uid, $fd){$result_uid = $this->userIdBind($uid, $fd);$result_fd = $this->fdBind($uid, $fd);if($result_uid && $result_fd) {return true;} else {return false;}}/*** Explain: get bind fd* @param $uid* User:runBaby* Date: 2019/8/3* Time: 6:28 PM* @return string*/public function getBindFd($uid){$field = $this->setRedisField($uid);$data = $this->redisObj->hGet($this->redisTable, $field);return $data;}/*** Explain: get bind user id* @param $fd* User:runBaby* Date: 2019/8/3* Time: 6:28 PM* @return string*/public function getBindUserId($fd){$field = $this->setRedisField($fd, 1);$data = $this->redisObj->hGet($this->redisTable, $field);return $data;}/*** Explain: unbinds userId and fd* @param $uid* User: runBaby* Date: 2019/8/3* Time: 6:38 PM* @return bool*/public function unbindFd($uid, $fd){//unbind userId$field_uid = $this->setRedisField($uid);$result_uid = $this->redisObj->hDel($this->redisTable, $field_uid);//unbind fd$field_fd = $this->setRedisField($fd, 1);$result_fd = $this->redisObj->hDel($this->redisTable, $field_fd);if($result_uid && $result_fd) {return true;} else {return false;}}}
我为人人,人人为我,美美与共,天下大同。