前言:
心跳:心脏还在跳动,说明还有生命迹象,还活着,还活着就表示还可以继续工作,生命不止,工作不息。
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;
}
}
}
我为人人,人人为我,美美与共,天下大同。