相手ホストのクラッシュを検知するために、用意されているソケットオプションにSO_KEEPALIVEがあります。
TCPソケットにSO_KEEPALIVEオプションが設定され、クライアントーサーバ間で2時間以上データが
交換されなかった場合、TCPは相手に向けてキープアライブプローブを送信します。
プローブの送信によって、発生する挙動は以下の3つ。
1. 相手がACKで応答した場合、アプリケーションには何の通信も行われません。
TCPは2時間後に新たなプローブを送信します。
2. 相手がRSTで応答した場合、TCPは相手のホストがクラッシュ後にリブートしたことが判明します。
ソケットはクローズされます。(ECONNRESETが設定される。)
3. キープアライブに応答がない場合、Ubuntuでは9つのプローブを75秒おきに送信して応答を確かめようとします。
最初のプローブ送信から11分15秒経過しても、応答が得られない場合は、プローブをあきらめます。
その後、ソケットはクローズされます。(ETIMEDOUTが設定される。)
また、ICMPエラーがルーターから返された場合も、ソケットはクローズされます。
ICMPエラーは、相手のホストはクラッシュしてはいないが、到達不可能(※)である場合に発生します。
(※)ルータの経路切断が該当する。
ここまで、各種設定数値が出てきましたが、これはUbuntuの場合下記のようにそれぞれ設定値を保持しています。
加えて、ソケットオプションの設定で値を変更することが出来ます。
TCP_KEEPIDLE
キープアライブを投げ始めるまでの時間(秒)デフォルトは7200秒(=2時間)
設定個所:/proc/sys/net/ipv4/tcp_keepalive_time
TCP_KEEPINTVL
キープアライブを投げる間隔(秒)デフォルトは75秒
設定個所:/proc/sys/net/ipv4/tcp_keepalive_intvl
TCP_KEEPCNT
キープアライブを投げリトライする回数。デフォルトは9回
設定個所:/proc/sys/net/ipv4/tcp_keepalive_probes
今回は、上記1と3.のケース、
キープアライブブローブ正常応答、
プローブに応答しない場合(ICMPエラー)について動作確認(wiresharkでパケットの計測)を行ってみます。
(サーバからクライアントへ3つのプローブに対して、いずれも応答を返さない)
動作環境
クライアント:Mac OS Lion(192.168.11.11)
エコークライアント。サーバipアドレスを指定して実行
サーバ:Ubuntu 12.04LTS(192.168.11.7)
エコーサーバ
ソケットオプション:TCP_KEEPIDLE=300/TCP_KEEPINTVL=30/TCP_KEEPCNT=3
キープアライブを投げ始めるまでの時間が2時間というのはちょっと長いので、今回は300秒(=5分)としました。
加えて、プローブ送信間隔を30秒、送信回数を3回へ変更しました。
クライアントプログラム
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
int maxfdp1;
struct sockaddr_in servaddr;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
FD_ZERO(&rset);
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
for (;;){
FD_SET(fileno(stdin), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(stdin), sockfd)+1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset)){
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if(FD_ISSET(fileno(stdin), &rset)){
if(Fgets(sendline, MAXLINE, stdin) == NULL)
return;
Writen(sockfd, sendline, strlen(sendline));
}
}
exit(0);
}
サーバプログラム
#include "unp.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
int
main(int argc, char **argv)
{
int listenfd, connfd;
int flag;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
flag = 1;
Setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flag, sizeof(flag));
flag = 300;
Setsockopt(listenfd, IPPROTO_TCP, TCP_KEEPIDLE, (void*)&flag, sizeof(flag) );
flag = 3;
Setsockopt(listenfd, IPPROTO_TCP, TCP_KEEPCNT, (void*)&flag, sizeof(flag) );
flag = 30;
Setsockopt(listenfd, IPPROTO_TCP, TCP_KEEPINTVL, (void*)&flag, sizeof(flag) );
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket }
}
計測結果
キープアライブブローブ正常応答のケース
パケットNo. 9614、9615で、キープアライブプローブとその応答がサーバークライアント間で実施されています。
以降、300秒ごとに、キープアライブプローブがサーバプロセスより送出されます。
キープアライブプローブ未応答のケース
サーバ側ターミナル上、netstat -anでソケットの接続状態をチェック。
192.168.11.7:9877 192.168.11.11:52464 ESTABLISHED
となり、接続は確立されています。
その後、クライアント側のネットワーク接続を切断。
300秒後、キープアライブタイマがタイムアウトするので、クライアントへ向けてキープアライブプローブが送信されています。
ただし、クライアント側はネットワーク切断しているので応答を返しません。
以降、30秒毎に、計2回、キーププローブを送り続けている様子が見えるのを想定していたのですが、
パケットが流れてる様子は見えませんでした。これ不思議。
60秒後、コネクション確立済みソケットがクローズされ終了。
サーバ側ターミナルnetstat -anでソケットの状態をチェックすると、確かにコネクションは解放されている。
192.168.11.7:9877 192.168.11.11:52464 ESTABLISHEDがない。