#1 はじめに
TCPについて、思いついたまま、いろいろ実験をしてみようと思います。
なお、TCPの各種状態の作り方は、TCPの各種状態の作り方を参照してください。
#2 環境
VMware Workstation 12 Player上のゲストマシンを使っています。
ゲストマシンはサーバとクライアントの2台構成です。
サーバ、クライアントともに下記設定です。
[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core)
[root@server ~]# uname -r
3.10.0-514.el7.x86_64
[root@server ~]# cat /etc/hosts
192.168.0.100 server
192.168.0.110 client
#3 CLOSE_WAITとは?
パッシブクローズ側がとる状態です。
アクティブクローズ側のFINに対してACKを返信し、
パッシブクローズ側がFINを送信するまでの状態です。
パッシブクローズ側のアプリケーションがclose()を実行しないと(バグやCPU負荷等)、
CLOSE_WAIT状態が残ったままになります。
ここでは、意図的にclose()を実行しないテストプログラムを作成して、
CLOSE_WAITのソケットが残ったままの状態を確認します。
なお、クライアント側はncコマンドを使います。
client server
(アクティブクローズ側) (パッシブクローズ側)
| |
| | # ./sv
| |
# nc server 11111|----------------- SYN ------------------->| -*-
|<---------------- SYN+ACK ----------------| | <= TCPコネクション確立
|----------------- ACK ------------------->| -*-
| |
| |
Ctrl +c ==>|------------------ FIN ------------------>| read()が戻り値0で復帰する。
|<----------------- ACK ------------------ | A
| | |
| | |
| | CLOSE_WAIT
| | |
| | V
|<----------------- FIN -------------------| close()
| | A
| | |
| | LAST_ACK
| | |
| | |
|------------------ ACK ------------------>| V
| |
| |
##3.1 サンプルプログラム
サーバ側で実行するサンプルプログラムです。
readシステムコールの戻り値が0(EOF受信時)の場合、300秒スリープするようになっています。
300秒の間に、サーバ側でソケットの状態を確認します。
なお、readシステムコール以外のシステムコールのエラー処理は省略しています。
[root@server tcp]# vi sv.c
[root@server tcp]# cat sv.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int lfd, cfd, optval=1;
socklen_t len;
struct sockaddr_in sv, cl;
char buf[32];
ssize_t n;
lfd = socket(AF_INET, SOCK_STREAM, 0);
sv.sin_family = AF_INET;
sv.sin_port = htons(11111);
sv.sin_addr.s_addr = INADDR_ANY;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
bind(lfd, (struct sockaddr *)&sv, sizeof(sv));
listen(lfd, 5);
len = sizeof(cl);
cfd = accept(lfd, (struct sockaddr *)&cl, &len);
while (1) {
memset(buf, 0, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n > 0)
fprintf(stderr,"%zd bytes received:%s", n, buf);
else if(n == 0){
fprintf(stderr,"EOF recieved. I'm going to sleep 300s.\n");
sleep(300);
}
else {
perror("read");
exit(1);
}
}
close(lfd);
exit(0);
}
テストプログラムをコンパイルします。
[root@server tcp]# gcc -Wall -o sv sv.c
ファイルを確認します。実行ファイルが作成できたことがわかります。
[root@server tcp]# ls
sv sv.c
##3.2 実験結果
closeシステムコールの呼び出しを遅らせることで、
CLOSE-WAIT状態のソケットを確認することができました。
テストプログラムを実行します。
[root@server tcp]# ./sv
もう1つターミナルを開きます。そして、lsofコマンドを実行します。
テストプログラムが11111番ポートでListenしていることがわかります。
[root@server ~]# lsof -i:11111 -P
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sv 1469 root 3u IPv4 27022 0t0 TCP *:11111 (LISTEN)
次に、ncコマンドを実行して、サーバの11111番ポートにTCPコネクションを確立します。
コネクション確立後、サーバにデータ(12345)を送信します。
[root@client tcp]# nc server 11111
12345
クライアントから、改行コードを含めて6バイト受信していることがわかります。
[root@server tcp]# ./sv
6 bytes received:12345
Ctrl + cを押下して、ncコマンドを終了する。
[root@client tcp]# nc server 11111
12345
^C
サーバ側でソケットの状態を確認する。ソケットの状態がCLOSE-WAITであることがわかる。
[root@server ~]# ss -nt4
State Recv-Q Send-Q Local Address:Port Peer Address:Port
CLOSE-WAIT★0 0 192.168.0.100:11111 192.168.0.110:40368
ESTAB 0 96 192.168.0.100:22 192.168.0.6:53776
ESTAB 0 0 192.168.0.100:22 192.168.0.6:52776
#6 listen()システムコールの第2引数
listen()の第2引数の意味について確認します。
##6.1 第2引数の意味
listen()の第2引数は、backlogキューのキュー長を表しています。
backlogキューとは、TCPコネクション確立済の接続要求をキューイングするキューです。
そして、アプリケーションがaccept()を実行すると、backlogキューから接続要求が取り出されます。
参考情報:listen backlog 【3.6】
##6.2 テストプログラム
サーバ側は下記テストプログラムを使います。クライアント側はncコマンドを使います。
ncコマンドは、ここを参照してください。
[root@server ~]# vi sv.c
[root@server ~]# cat sv.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <errno.h>
int main(int argc, char *argv[])
{
int sock0, sock;
struct sockaddr_in sv, cl;
socklen_t len;
int pid, cpid;
char buf[1024];
ssize_t n;
int status;
sock0 = socket(AF_INET, SOCK_STREAM, 0);
sv.sin_family = AF_INET;
sv.sin_port = htons(11111);
sv.sin_addr.s_addr = INADDR_ANY;
bind(sock0, (struct sockaddr *)&sv, sizeof(sv));
listen(sock0, 3);
for (;;) {
len = sizeof(cl);
sock = accept(sock0, (struct sockaddr *)&cl, &len);
pid = fork();
if (pid == 0) {
close(sock0);
while(1) {
n = read(sock, buf, sizeof(buf));
if(n > 0) {
fprintf(stderr,"PID=%d:%zd bytes received:%s", getpid(), n, buf);
write(sock, buf, n);
}
else if(n == 0) {
fprintf(stderr,"EOF recieved. I'm going to exit(%d)\n", getpid());
close(sock);
exit(0);
}
else {
perror("read");
exit(1);
}
}
}
else {
close(sock);
while ((cpid = waitpid(-1, &status, WNOHANG)) > 0);
}
}
close(sock0);
exit(0);
}
##6.3 実験結果
listenシステムコールの第2引数(backlog)の値を変化させたときの挙動を確認する。
listen(fd, 2)とlisten(fd, 3)の場合について、確立できるTCPコネクション数を確認する。
###6.3.1 backlogが2の場合
テストプログラムを起動する。backlogが2(★)であることがわかる。
Ctrl + zを押下して、テストプログラムを停止する。
[root@server ~]# strace ./sv
-中略-
listen(3, ★2) = 0
accept(3, ^Z
[1]+ 停止 strace ./sv
サーバにTCPコネクションを4つ確立する。
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
ジョブの数を確認する。4つ起動していることがわかる。
[root@client ~]# jobs
[5] 停止 nc server 11111
[6] 停止 nc server 11111
[7] 停止 nc server 11111
[8]- 停止 nc server 11111
サーバ側でTCPコネクション数を確認する。しかし、確立したTCPコネクション数は3つであることがわかる。
[root@server ~]# ss -nt4 'sport == 11111'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:51994
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:51992
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:51996
###6.3.2 backlogが3の場合
テストプログラムを起動する。backlogが3(★)であることがわかる。
Ctrl + zを押下して、テストプログラムを停止する。
[root@server ~]# strace ./sv
-中略-
listen(3, ★3) = 0
accept(3, ^Z
[1]+ 停止 strace ./sv
サーバにTCPコネクションを5つ確立する。
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
[root@client ~]# nc server 11111&
ジョブの数を確認する。5つ起動していることがわかる。
[root@client ~]# jobs
[1] 停止 nc server 11111
[2] 停止 nc server 11111
[3] 停止 nc server 11111
[4]- 停止 nc server 11111
[5]+ 停止 nc server 11111
サーバ側でTCPコネクション数を確認する。しかし、確立したTCPコネクション数は4つであることがわかる。
[root@server ~]# ss -nt4 'sport == 11111'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:52016
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:52010
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:52012
ESTAB 0 0 192.168.0.100:11111 192.168.0.110:52014
##6.4 まとめ
backlog+1まで、TCPコネクションが確立できることがわかった。
#8 再送について
TCPでは、信頼性を保つため、様々な状況で再送を行っています。
再送回数はカーネルパラメータで設定できますが、目安にしかすぎません。
再送回数は,カーネルパラメータ、RTO等を元に算出しています。
番号 | パケットの名称 | カーネルパラメータ | パケットの概要 |
---|---|---|---|
1 | SYNパケット | net.ipv4.tcp_syn_retries | TCPコネクション確立時、アクティブオープ側が送信するパケット |
2 | SYN+ACKパケット | net.ipv4.tcp_synack_retries | TCPコネクション確立時、パッシブオープン側が送信するパケット。1の応答 |
3 | データパケット | net.ipv4.tcp_retries2 | TCPコネクション確立後に送信するデータパケット |
4 | FINパケット | net.ipv4.tcp_orphan_retries | TCPコネクション終了時に送信するパケット |
5 | keepaliveパケット | net.ipv4.tcp_keepalive_probes | 一定間隔で相手の生死を確認するパケット |
##8.4 FINの再送回数(★★検証中★★-検証のたびに回数が異なるんですよねぇ。。)
FINに対するACKが返ってこないと、FINを再送します。
FINの再送回数はtcp_orphan_retriesで決まります。tcp_orphan_retriesのデフォルト値は0です。
tcp_orphan_retriesの値が0だからといってFINの再送回数が0ということではありません。
FINの再送回数は以下のようになりました。iptablesとsystemtapを使って確認をしました。
tcp_orphan_retriesの値 | FINの再送回数 | FINの送信回数(再送回数を含めた回数) |
---|---|---|
0(デフォルト値) | x回 | x回 |
1 | x回 | x回 |
2 | x回 | x回 |
3 | x回 | x回 |
以下、略 |
以下は、デフォルト値(0)の場合におけるFINの再送を示したシーケンスです。
client server
(アクティブクローズ側) (パッシブクローズ側)
| |
| | # iptables -I INPUT -p tcp --tcp-flags FIN FIN -j DROP
| | # nc -kl 11111
| |
# nc server 11111 |--------------- SYN --------------------->| -*-
|<-------------- SYN +ACK -----------------| | <= TCPコネクション確立
|--------------- ACK --------------------->| -*-
| |
| |
Ctrl + c -*- |--------------- FIN --------------------->| 廃棄
| | |
| |--------------- FIN(Retrans 1 ) --------->| 廃棄
| |--------------- FIN(Retrans 2 ) --------->| 廃棄
| |--------------- FIN(Retrans 3 ) --------->| 廃棄
FIN-WAIT-1 |--------------- FIN(Retrans 4 ) --------->| 廃棄
| |--------------- FIN(Retrans 5 ) --------->| 廃棄
| |--------------- FIN(Retrans 6 ) --------->| 廃棄
| |--------------- FIN(Retrans 7 ) --------->| 廃棄
| |--------------- FIN(Retrans 8 ) --------->| 廃棄
| | |
-*- |--------------- FIN(Retrans X ) --------->| 廃棄
| | |
CLOSED | |
| | |
#X 参考情報
Programming UNIX Sockets in C - Frequently Asked Questions
Chapter 13. カーネルのネットワークパラメータ