TCP実験室

#1 はじめに
TCPについて、思いついたまま、いろいろ実験をしてみようと思います。
なお、TCPの各種状態の作り方は、[TCPの各種状態の作り方](https://qiita.com/hana_shin/items/f9bdfaebdfd078291a5a)を参照してください。

#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コマンド](https://qiita.com/hana_shin/items/97e6c03ac5e5ed67ce38)を使います。

```
                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】](http://wiki.bit-hive.com/linuxkernelmemo/pg/listen%20backlog%20%A1%DA3.6%A1%DB)

##6.2 テストプログラム
サーバ側は下記テストプログラムを使います。クライアント側はncコマンドを使います。
ncコマンドは、[ここ](https://qiita.com/hana_shin/items/97e6c03ac5e5ed67ce38)を参照してください。

```
[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](http://www.kt.rim.or.jp/~ksk/sock-faq/unix-socket-faq-ja.html#toc2)
[Chapter 13. カーネルのネットワークパラメータ](https://linuxjf.osdn.jp/JFdocs/Adv-Routing-HOWTO/lartc.kernel.html)

[TCPの再送タイムアウトを制御したい](http://thatsdone-j.blogspot.jp/2013/02/tcp.html)
[TCP User Timeout Option](https://tools.ietf.org/html/rfc5482)