什么是心跳呢?
心跳用于判斷一個連接是正常還是斷開的狀態(tài)
例如:TCP中使用五元組標識一個網絡連接,創(chuàng)建TCP連接時會發(fā)生三次握手,斷開TCP連接時會發(fā)生四次揮手。不管是服務器還是客戶端發(fā)起連接到關閉,都會經歷完整的四次揮手的階段。最后由系統(tǒng)回收客戶端的文件描述符fd,應用層也可以使用onClose進行回調處理。
文件描述符
什么是文件描述符fd呢?
UNIX哲學中一切皆是文件,文件描述符fd是系統(tǒng)層暴露給業(yè)務層,用來表示一個五元組網絡連接的標識,可以簡單理解為索引,通過對文件描述符的操作,系統(tǒng)層可以找到相應的連接并進行一系列的操作,如發(fā)送數據到網絡、連接關閉等。
Swoole中文件描述符fd是一個自增的整型數字,取值范圍為1到1600萬,fd超過1600萬之后會自動從1開始進行復用。文件描述符之所以是一個整型的數字而非對象,主要原因是Swoole是多進程模型,在Worker進程或Task進程中隨時可能要訪問某個客戶端連接,如果使用對象就需要進行序列化和反序列化,這樣就會增加額外的性能開銷,采用整型數字的好處就可以直接存儲傳輸被使用。
為什么系統(tǒng)需要回收文件描述符fd呢?
如果需關閉某個連接,可以在業(yè)務層對文件描述符fd發(fā)起關閉連接的操作,文件描述符對于操作系統(tǒng)而言是有限的資源,必須重復利用,所以必須要回收。
$server->close($fd);
心跳機制
為什么會出現心跳機制呢?
正常情況下,客戶端中斷TCP連接時會發(fā)送一個FIN包進行四次斷開握手來通知服務器,但在某些異常情況下,比如突然斷網掉線或網絡異常,服務端并不能夠感知到連接的異常,而實際上連接可能已經失效。尤其在移動網絡中,TCP連接非常不穩(wěn)定,必須有一套機制來確保服務器和客戶端之間連接的有效性。如果沒有回收機制,這種連接會耗盡所有文件描述符fd,導致系統(tǒng)不再能夠接收新的連接請求,因此就有了心跳機制。
什么是心跳機制呢?
心跳機制是業(yè)務層提供的一種判斷連接是否仍舊存活的方式,讓系統(tǒng)能夠感知一個連接是否失效。
系統(tǒng)層面會提供心跳機制,但粒度粗糙時間稍長,更重要的是沒有應用層靈活。
心跳機制有兩種實現方式
客戶端定時發(fā)送一個心跳包,告知服務器連接仍舊還活著,服務器會定時檢測所有客戶端列表,查看最后一個心跳包的時間是否過長,如果時間過長則認定已無心跳,進而判定為死連接,并主動關閉這個連接。
客戶端定時發(fā)送心跳包的方式,對服務器和網絡的壓力更小,更加靈活。服務器定時詢問所有客戶端是否還存活,如果仍然存活則客戶端給出反饋,否則認定為死連接并主動關閉。
服務器定時詢問的方式,對服務器和網絡的壓力更大,不推薦使用。
什么是心跳包呢?
從客戶端到服務器這條巨大的鏈路中會經過無數的路由器,每個路由器都可能會檢測多少秒時間內沒有數據包,則會自動關閉連接的節(jié)能機制。為了讓這個可能會出現的節(jié)能機制失效,客戶端可以設置一個定時器,每隔固定時間發(fā)送一個隨機字符一字節(jié)的數據包,這種數據包就是心跳包。
Swoole的心跳機制是如何實現的呢?
對于多數TCP網絡服務器都會考慮心跳機制,TCP的keepalive選項可以用來檢測死連接,只要客戶端沒有死掉,服務器會在超過keepidle閑置事件后發(fā)送一個TCP探測包,發(fā)送次數是tcp_keepcount次,每次間隔時間是tcp_keepinterval,如果客戶端沒有發(fā)送ack確認,服務器才會關閉連接。
在TCP中有一個Keep-Alive的機制可以檢測死連接,應用層對于死連接周期不敏感或沒有實現心跳機制,可以使用操作系統(tǒng)提供的keepalive機制來踢掉死連接。Keep-Alive機制不會強制切換連接,如果連接存在但一直不發(fā)生數據交互,Keep-Alive也不會切斷連接。而應用層實現的心跳檢測heartbeat_check即使連接存在,在不產生數據交互的情況下,依然會強制切斷連接。
Swoole實現的心跳機制,只要客戶端超過一定時間沒有發(fā)送數據,不管這個連接是不是死連接,都會關閉掉。
Swoole提供了ping功能,通過配置ping值,Swoole內核可以判斷當只有一個心跳包時不會將數據包轉發(fā)給應用層onReceive。
Swoole采用客戶端定時發(fā)送心跳包服務端定時檢測的方式,Swoole會在Master主進程中獨立創(chuàng)建一個心跳線程,通過定時輪詢所有客戶端連接的方式,來判斷連接是否已經失效,因此Swoole的心跳并不會堵塞任何業(yè)務邏輯。
Swoole使用心跳前需要提前配置服務器運行時參數,其中有兩個配置參數:
-
heartbeat_check_interval
設置服務器定時檢測在線列表的時間間隔 -
heartbeat_idle_time
設置連接最大的空閑時間,如果最后一個心跳包的時間與當前時間只差超過設定值則認為連接失效。
例如:
$config = [];
// 設置每5秒服務器會偵測一次心跳
$config["heartbeat_check_interval"] = 5;
// 設置一個TCP連接如果在10秒內未向服務器發(fā)送數據則被切斷
$config["heartbeat_idle_time"] = 10;
$server->set($config);
建議heartbeat_idle_time比heartbeat_check_interval的值多兩倍多,兩倍是為了進行容錯允許丟包,多一點兒是考慮到網絡延時的情況,這個可以根據實際的業(yè)務情況調整容錯率。
另外,Swoole提供了swoole_server::heartbeat()方法用于手工檢測心跳是否到期,heartbeat方法發(fā)會返回閑置時間超過heartbeat_idle_time的所有TCP連接,應用程序可以在這些連接中做一些操作,如發(fā)送數據或關閉連接等。
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時,底層會屏蔽onConnect和onClose事件,因此也就無法回調close關閉事件了。
Swoole的心跳機制是如何判斷連接是否還處于存活狀態(tài)的呢?
Swoole擴展內置的心跳機制,在每次接收到客戶端數據時會記錄一個時間戳,當客戶端在一定時間內沒有向服務器發(fā)送數據時,服務器會自動切斷連接。
在Swoole中connection連接的結構體中有一個time_t last_time字段,用于存放最后一次收包的時間戳,通過時間戳對比來判定連接是否存活。
心跳檢測
服務器
$ vim server.php
<?php
class Server
{
private $server;
public function __construct($host, $port, $config)
{
//創(chuàng)建服務器
$this->server = new swoole_server($host, $port);
//設置運行時參數
$this->server->set($config);
//設置監(jiān)聽
$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