Swoole Heartbeat 心跳

什么是心跳呢?

心跳用于判断一个连接是正常还是断开的状态

例如:TCP中使用五元组标识一个网络连接,创建TCP连接时会发生三次握手,断开TCP连接时会发生四次挥手。不管是服务器还是客户端发起连接到关闭,都会经历完整的四次挥手的阶段。最后由系统回收客户端的文件描述符fd,应用层也可以使用onClose进行回调处理。

文件描述符

什么是文件描述符fd呢?

UNIX哲学中一切皆是文件,文件描述符fd是系统层暴露给业务层,用来表示一个五元组网络连接的标识,可以简单理解为索引,通过对文件描述符的操作,系统层可以找到相应的连接并进行一系列的操作,如发送数据到网络、连接关闭等。

Swoole中文件描述符fd是一个自增的整型数字,取值范围为1到1600万,fd超过1600万之后会自动从1开始进行复用。文件描述符之所以是一个整型的数字而非对象,主要原因是Swoole是多进程模型,在Worker进程或Task进程中随时可能要访问某个客户端连接,如果使用对象就需要进行序列化和反序列化,这样就会增加额外的性能开销,采用整型数字的好处就可以直接存储传输被使用。

为什么系统需要回收文件描述符fd呢?

如果需关闭某个连接,可以在业务层对文件描述符fd发起关闭连接的操作,文件描述符对于操作系统而言是有限的资源,必须重复利用,所以必须要回收。

$server->close($fd);

心跳机制

为什么会出现心跳机制呢?

正常情况下,客户端中断TCP连接时会发送一个FIN包进行四次断开握手来通知服务器,但在某些异常情况下,比如突然断网掉线或网络异常,服务端并不能够感知到连接的异常,而实际上连接可能已经失效。尤其在移动网络中,TCP连接非常不稳定,必须有一套机制来确保服务器和客户端之间连接的有效性。如果没有回收机制,这种连接会耗尽所有文件描述符fd,导致系统不再能够接收新的连接请求,因此就有了心跳机制。

什么是心跳机制呢?

心跳机制是业务层提供的一种判断连接是否仍旧存活的方式,让系统能够感知一个连接是否失效。

系统层面会提供心跳机制,但粒度粗糙时间稍长,更重要的是没有应用层灵活。

心跳机制有两种实现方式

  1. 客户端定时发送一个心跳包,告知服务器连接仍旧还活着,服务器会定时检测所有客户端列表,查看最后一个心跳包的时间是否过长,如果时间过长则认定已无心跳,进而判定为死连接,并主动关闭这个连接。
    客户端定时发送心跳包的方式,对服务器和网络的压力更小,更加灵活。

  2. 服务器定时询问所有客户端是否还存活,如果仍然存活则客户端给出反馈,否则认定为死连接并主动关闭。
    服务器定时询问的方式,对服务器和网络的压力更大,不推荐使用。

什么是心跳包呢?

从客户端到服务器这条巨大的链路中会经过无数的路由器,每个路由器都可能会检测多少秒时间内没有数据包,则会自动关闭连接的节能机制。为了让这个可能会出现的节能机制失效,客户端可以设置一个定时器,每隔固定时间发送一个随机字符一字节的数据包,这种数据包就是心跳包。

Swoole的心跳机制是如何实现的呢?

对于多数TCP网络服务器都会考虑心跳机制,TCP的keepalive选项可以用来检测死连接,只要客户端没有死掉,服务器会在超过keepidle闲置事件后发送一个TCP探测包,发送次数是tcp_keepcount次,每次间隔时间是tcp_keepinterval,如果客户端没有发送ack确认,服务器才会关闭连接。

在TCP中有一个Keep-Alive的机制可以检测死连接,应用层对于死连接周期不敏感或没有实现心跳机制,可以使用操作系统提供的keepalive机制来踢掉死连接。Keep-Alive机制不会强制切换连接,如果连接存在但一直不发生数据交互,Keep-Alive也不会切断连接。而应用层实现的心跳检测heartbeat_check即使连接存在,在不产生数据交互的情况下,依然会强制切断连接。

Swoole实现的心跳机制,只要客户端超过一定时间没有发送数据,不管这个连接是不是死连接,都会关闭掉。

Swoole提供了ping功能,通过配置ping值,Swoole内核可以判断当只有一个心跳包时不会将数据包转发给应用层onReceive

Swoole采用客户端定时发送心跳包服务端定时检测的方式,Swoole会在Master主进程中独立创建一个心跳线程,通过定时轮询所有客户端连接的方式,来判断连接是否已经失效,因此Swoole的心跳并不会堵塞任何业务逻辑。

Swoole使用心跳前需要提前配置服务器运行时参数,其中有两个配置参数:

  • heartbeat_check_interval
    设置服务器定时检测在线列表的时间间隔
  • heartbeat_idle_time
    设置连接最大的空闲时间,如果最后一个心跳包的时间与当前时间只差超过设定值则认为连接失效。

例如:

$config = [];
// 设置每5秒服务器会侦测一次心跳
$config["heartbeat_check_interval"] = 5;
// 设置一个TCP连接如果在10秒内未向服务器发送数据则被切断
$config["heartbeat_idle_time"] = 10;
$server->set($config);

建议heartbeat_idle_timeheartbeat_check_interval的值多两倍多,两倍是为了进行容错允许丢包,多一点儿是考虑到网络延时的情况,这个可以根据实际的业务情况调整容错率。

另外,Swoole提供了swoole_server::heartbeat()方法用于手工检测心跳是否到期,heartbeat方法发会返回闲置时间超过heartbeat_idle_time的所有TCP连接,应用程序可以在这些连接中做一些操作,如发送数据或关闭连接等。

swoole_server::heartbeat()

注意,使用swoole_server::heartbeat()方法前,如果设置了heartbeat_check_interval配置选项,将会关闭超时的连接。否则会返回过时连接。

// 手工关闭超时连接
$serverr->tick(1000, funcntion($id) use($server){
  $fds = $server->heartbeat(false);
  foreach($fds as $fd){
    $server->close($fd);
  }
});

另外,如果提前设置了dispatch_mode为1或3时,底层会屏蔽onConnectonClose事件,因此也就无法回调close关闭事件了。

Swoole的心跳机制是如何判断连接是否还处于存活状态的呢?

Swoole扩展内置的心跳机制,在每次接收到客户端数据时会记录一个时间戳,当客户端在一定时间内没有向服务器发送数据时,服务器会自动切断连接。

在Swoole中connection连接的结构体中有一个time_t last_time字段,用于存放最后一次收包的时间戳,通过时间戳对比来判定连接是否存活。

心跳检测

服务器

$ vim server.php
<?php
class Server
{
    private $server;
    public function __construct($host, $port, $config)
    {
        //创建服务器
        $this->server = new swoole_server($host, $port);
        //设置运行时参数
        $this->server->set($config);
        //设置监听
        $this->server->on("Start", [$this, "onStart"]);
        $this->server->on("Connect", [$this, "onConnect"]);
        $this->server->on("Receive", [$this, "onReceive"]);
        $this->server->on("Close", [$this, "onClose"]);
        //开启服务器
        $this->server->start();
    }
    public function onStart($server)
    {
        echo "[start] master {$server->master_pid} manager {$server->manager_pid}".PHP_EOL;
    }
    public function onConnect($server, $fd, $reactor_id)
    {
        echo "[connect] reactor {$reactor_id} worker {$server->worker_pid} client {$fd}".PHP_EOL;
    }
    public function onReceive($server, $fd, $reactor_id, $data)
    {
        echo "[receive] reactor {$reactor_id} worker {$server->worker_pid} client {$fd}: {$data}".PHP_EOL;
        $server->send($fd, $data);
    }
    public function onClose($server, $fd)
    {
        echo "[close] client {$fd} close".PHP_EOL;
    }
}

$config = [];
$config["worker_num"] = 8;
$config["daemonize"] = 0;
$config["max_request"] = 1000;
$config["dispatch_mode"] = 2;
$config["debug_mode"] = 1;
$config["log_file"] = "/swoole.log";
$config["heartbeat_check_interval"] = 5;
$config["heartbeat_idle_time"] = 10;
$host = "0.0.0.0";
$port = 9000;
$server = new Server($host, $port, $config);

客户端

$ vim client.php
<?php
class Client
{
    private $client;

    public function __construct($host, $port)
    {
        $this->client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
        $this->client->on("Connect", [$this, "onConnect"]);
        $this->client->on("Receive", [$this, "onReceive"]);
        $this->client->on("Close", [$this, "onClose"]);
        $this->client->on("Error", [$this, "onError"]);
        if(!$fp = $this->client->connect($host, $port)){
            echo "error {$fp->errCode} {$fp->errMsg}";
            return;
        }
    }
    public function onConnect($client)
    {
        fwrite(STDOUT, "send: ");
        swoole_event_add(STDIN, function(){
            fwrite(STDOUT, "send: ");
            $this->client->send(trim(fgets(STDIN)));
        });
    }
    public function onReceive($client, $data)
    {
        echo $data.PHP_EOL;
    }
    public function onClose($client)
    {
        echo "close".PHP_EOL;
    }
    public function onError($client)
    {
        echo "error {$client->errCode} {$client->errMsg}".PHP_EOL;
    }
}
$client = new Client("127.0.0.1", 9000);

运行服务器

$ php server.php
[start] master 539 manager 540

运行客户端

$ php client.php
send: 

观察客户端

$ php client.php
send: HELLO
send: HELLO
close

观察服务器

[start] master 539 manager 540
[connect] reactor 0 worker 544 client 1
[receive] reactor 0 worker 544 client 1: HELLO
[close] client 1 close
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值