hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

ソケットオプションの使い方(SO_REUSEADDR編)



1 SO_REUSEADDRソケットオプションとは?

SO_REUSEADDRは、TIME-WAIT状態のソケットの名前(ローカルIPアドレスとローカルポート番号の組)と同じ名前をソケットにバインドすることができるソケットオプションです。なお、サーバのリスニングソケットには、SO_REUSEADDRを設定するのが一般的です。

2 検証環境

2.1 ネットワーク構成

サーバとクライアントの2台構成です。図中のenp1s0はNICの名前です。

                               192.168.122.0/24
client(enp1s0) ------------------------------------------(enp1s0) server
            .177                                       .68

2.2 版数

サーバとクライアントのAlmaLinuxの版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 8.6 (Sky Tiger)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
4.18.0-372.9.1.el8.x86_64

3 事前準備

クライアントからサーバのTCPの11111番ポートにアクセスできるようにするため、11111番ポートを解放します。なお、firewall-cmdの使い方は、firewall-cmdの使い方は、firewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# firewall-cmd --add-port=11111/tcp
success

ルールを確認します。11111番ポートへのアクセスを許可するルールが追加されたことがわかります。

[root@server ~]# firewall-cmd --list-ports
11111/tcp

4 テストプログラム(以降TP)の作成

サーバ、クライアントで実行するTPを作成します。ソースコードを見やすくするため、意図的に異常処理は省略しています。また、ソケットオプションの動作確認を目的としているため、実用的なプログラムにはなっていません。

4.1 サーバ側

TCPコネクションが確立すると、acceptシステムコールが復帰します。そして、forkシステムコールを実行して子プロセスを生成します。子プロセスは自身のPIDを出力した後、closeシステムコールを実行してTCPコネクションを終了します。closeシステムコールを実行すると、サーバからクライアントにFINパケットが送信されます。サーバ側が先にcloseシステムコールを実行するので、アクティブクローズ側になります。なお、TPは次のように使用します。引数に1を指定すると、SO_REUSEADDRオプションが有効になります。無効にする場合は1以外を指定してください。引数を指定しないとcore dumpします。

[root@server ~]# cat sv.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <netinet/in.h>

int child_prcess(int cfd);

int main(int argc, char *argv[])
{
  int lfd, cfd, status, on=1;
  socklen_t len;
  struct sockaddr_in sv, cl;
  pid_t pid;

  lfd = socket(AF_INET, SOCK_STREAM, 0);
  sv.sin_family = AF_INET;
  sv.sin_port = htons(11111);
  sv.sin_addr.s_addr = INADDR_ANY;

  if((atoi(argv[1]) == 1)){
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
  }

  if(bind(lfd, (struct sockaddr *)&sv, sizeof(sv)) < 0){
    perror("bind");
    exit(1);
  }

  listen(lfd, 5);

  while (1) {
    len = sizeof(cl);
    cfd = accept(lfd, (struct sockaddr *)&cl, &len);
    printf("accept\n");
    if((pid=fork()) == 0){  //child
      close(lfd);
      child_prcess(cfd);
      close(cfd);
      exit(0);
    }
    else{    // parent
      close(cfd);
      waitpid(pid, &status, 0);
      exit(0);
    }
  }
}

int child_prcess(int cfd)
{
  printf("child PID=%d\n", getpid());
  return 0;
}

コンパイルします。

[root@server ~]# gcc -Wall -o sv sv.c

4.2 クライアント側

connectシステムコールを実行して、サーバとTCPコネクションを確立します。その後、readシステムコールを実行してデータ受信待ちとなります。readシステムコールの戻り値は0かどうかで判定しています。どのような状況のときに戻り値が0になるかを説明すると、サーバのTPが終了するとcloseシステムコールが実行され、その延長でサーバからクライアントにFINパケットが送信されます。クライアントがFINパケットを受信すると、readシステムコールの戻り値が0で復帰します。なお、readシステムコールの戻り値は、戻り値が正の場合(受信バイト数)、0の場合(FIN受信)、負の場合(異常終了)の3つで場合わけするのが正しいです。

[root@client ~]# cat cl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
  int sock;
  struct sockaddr_in server;
  ssize_t n;
  char buf[32];

  sock = socket(AF_INET, SOCK_STREAM, 0);
  server.sin_family = AF_INET;
  server.sin_port = htons(11111);
  server.sin_addr.s_addr = inet_addr("192.168.122.68");

  connect(sock, (struct sockaddr *)&server, sizeof(server));
  n = read(sock, buf, sizeof(buf));
  if(n == 0) {
    printf("we received FIN from server.\n");
    close(sock);
    exit(0);
  }
  else {
    exit(0);
  }
}

コンパイルします。

[root@client ~]# gcc -Wall -o cl cl.c

4.3 シーケンス

TP実行時のシーケンスは以下のようになります。TIME-WAIT状態のときにサーバのTPを実行します。

                     client                              server(11111)
                        |                                   |
                        |                                   | <-- execute server's TP
                        |                                   |
 execute client's TP -->|                                   | socket()
                        |                                   | bind()
                        |                                   | listen()
                        |                                   |
              connect() |------------- SYN ---------------->|
                        |<-------------SYN + ACK -----------|
                        |------------- ACK ---------------->| accept() returns cfd
                        |                                   |
                        |                                   |
       read() returns 0 |<------------ FIN -----------------| close()
                        |------------- ACK ---------------->|
                        |                                   |
                close() |------------- FIN ---------------->|
                        |<------------ ACK -----------------| -*-
                        |                                   |  |
                        |                                   |  | <-- execute server's TP 
                        |                                   |  |
                        |                                   | TIME-WAIT
                        |                                   |  |
                        |                                   |  |
                        |                                   | -*-
                        |                                   |

5 検証結果

5.1 SO_REUSEADDRを指定しない場合

サーバでTPを実行します。このとき、引数に0を指定してSO_REUSEADDRオプションを使用しないようにします。

[root@server ~]# ./sv 0

ssコマンドを実行して、ソケットの状態を確認するとListen状態であることがわかります。なお、ssコマンドの使い方は、ssコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# ss -anot4 'sport == :11111'
State               Recv-Q              Send-Q                           Local Address:Port                              Peer Address:Port              Process
LISTEN              0                   5                                      0.0.0.0:11111                                  0.0.0.0:*

クライアントでTPを実行します。サーバとTCPコネクションが確立したあと、サーバがクライアントにFINパケットを送信します。クライアントがFINパケットを受信するとreadシステムコールが0で復帰して、下記メッセージを出力します。

[root@client ~]# ./cl
we received FIN from server.

ssコマンドを実行して、ソケットの状態を確認するとTIME-WAIT状態であることがわかります。

[root@server ~]# ss -anot4 'sport == :11111'
State                 Recv-Q             Send-Q                          Local Address:Port                               Peer Address:Port              Process
TIME-WAIT             0                  0                              192.168.122.68:11111                           192.168.122.177:60844              timer:(timewait,57sec,0)

TIME-WAIT状態のときにサーバでTPを実行すると、エラーが発生します(期待値)。

[root@server ~]# ./sv 0
bind: Address already in use

5.2 SO_REUSEADDRを指定した場合

サーバでTPを実行します。このとき、引数に1を指定してSO_REUSEADDRオプションを使用します。

[root@server ~]# ./sv 1

ssコマンドを実行して、ソケットの状態を確認するとListen状態であることがわかります。

[root@server ~]# ss -anot4 'sport == :11111'
State               Recv-Q              Send-Q                           Local Address:Port                              Peer Address:Port              Process
LISTEN              0                   5                                      0.0.0.0:11111                                  0.0.0.0:*

クライアントでTPを実行します。サーバとTCPコネクションが確立したあと、サーバがクライアントにFINパケットを送信します。クライアントがFINパケットを受信するとreadシステムコールが0で復帰して、下記メッセージを出力します。

[root@client ~]# ./cl
we received FIN from server.

ssコマンドを実行して、ソケットの状態を確認するとTIME-WAIT状態であることがわかります。

[root@server ~]# ss -anot4 'sport == :11111'
State                 Recv-Q             Send-Q                          Local Address:Port                               Peer Address:Port              Process
TIME-WAIT             0                  0                              192.168.122.68:11111                           192.168.122.177:60848              timer:(timewait,56sec,0)

TIME-WAIT状態のときにサーバでTPを実行しても、今回はエラーが発生しないことがわかります(期待値)。

[root@server ~]# ./sv 1

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ