LoginSignup
17
20

More than 1 year has passed since last update.

TCP実験室

Last updated at Posted at 2017-11-13

#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. カーネルのネットワークパラメータ

TCPの再送タイムアウトを制御したい
TCP User Timeout Option

17
20
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
20