2015年4月12日日曜日

和人くんのSO_REUSEADDRオプション癖が抜けない件

和人くん、マジで知ってる!? socketのSO_REUSEADDRオプション


最近マジ、和人のソケット使いが荒すぎて困ってます、そんな時は

「まだ使われていますので、後ほどご利用ください...」

と丁寧にお断りしてください。


 ※画像はイメージです

それでは本題です


サーバー系のコードを見たりしてると、socketを作成しbind()する前にsetsockopt()でSO_REUSEADDRするのがほとんどですが、素直に...

「何でだよ、何でだよ、」

って2回ぐらいおもってしまいましたので、調べました、というか、ほとんど忘れてました。

後から気づきましたが、このソケットオプションを試すのに、こんなやりすぎたコードは必要なかったのですが、書いてしまったので載せます、コードが汚いので何をやっているのか先に図をご確認ください


動作の流れは、

  • 「和人かどうかわからないプロセス」「たぶん和人だと思われるプロセス」をforkします(これがサーバープロセスです)
  • 「たぶん和人だと思われるプロセス」はポート9000でLISTENします
  • 「和人かどうかわからないプロセス」はその後さらに「和人の子」をforkします(これがクライアントプロセスです)
  • 「和人の子」は最大5個まで創られます(どうやって創られたかは不明です)
  • 「和人の子」がいなくなると「和人かどうかわからないプロセス」はSIGCHLDシグナルを受けとりますので、しっかりwaipid()で受け止めて葬り去ります 
process数が腐らないようにsleep()で逃げたり、シグナルハンドラの中でこの関数呼んじゃダメでしょ、とか突っ込まないようにお願いします。


ビルド方法


Linux
$ gcc -g -O0 so_reuseaddr.c -o no_reuse ・・・ SO_REUSEADDRなし
$ gcc -g -O0 so_reuseaddr.c -o no_reuse -DKAZUTO・・・SO_REUSEADDRあり

Solaris
cc -g -O0 so_reuseaddr.c -lsocket -lnsl -o no_reuse
cc -g -O0 so_reuseaddr.c -lsocket -lnsl -DKAZUTO -o reuse
とやりますとそれぞれの実行可能形式のファイルが出来上がります。

試してみる


* SO_REUSEADDRオプションを有効にしない場合
cuomo@karky7 ~ $ ./no_reuse 
和人は創りました(1号) pid=4243
和人の子 pid=4243 は暇人なので 9秒だけ寝るw..
和人は創りました(2号) pid=4245
和人の子 pid=4245 は暇人なので 9秒だけ寝るw..
和人は創りました(3号) pid=4246
和人の子 pid=4246 は暇人なので 6秒だけ寝るw..
和人は創りました(4号) pid=4264
和人の子 pid=4264 は暇人なので 7秒だけ寝るw..
和人は創りました(5号) pid=4265
和人の子 pid=4265 は暇人なので 3秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58599 <-- accept()が起きた
和人の子供がオワタ(5号) 4246
和人は創りました(5号) pid=4266
和人の子 pid=4266 は暇人なので 3秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58600
...
...
和人が親なんて信じられるかっ!! もう出るわ!
和人の子供がオワタ(5号) 4266
和人は創りました(5号) pid=4273
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58605
和人が親なんて信じられるかっ!! もう出るわ!
和人の子供がオワタ(5号) 4269
和人は創りました(5号) pid=4274
割り込み
...
...
cuomo@karky7 ~ $ ./no_reuse <----- 2回目の実行
和人は創りました(1号) pid=4277
ああぁー、やっちまった...: Address already in use <---- まだ使えない
和人の子供がオワタ(1号) 4276
和人は創りました(1号) pid=4278
和人の子 pid=4278 は暇人なので 3秒だけ寝るw..
和人の子 pid=4277 は暇人なので 3秒だけ寝るw..
和人は創りました(2号) pid=4280
和人の子 pid=4280 は暇人なので 3秒だけ寝るw..
割り込み
...
...
cuomo@karky7 ~ $ netstat -tpn
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 192.168.254.21:58603      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:9000       192.168.254.21:58605      TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58602      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:9000       192.168.254.21:58604      TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58599      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58600      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58601      192.168.254.21:9000       TIME_WAIT   -                   
cuomo@karky7 ~ $

SO_REUSEADDRオプションをつけないと2回め以降のbind()が「Address already in use」で失敗し、サーバープロセスが起動できません。よって和人の子は孤児のまま彷徨う事になります。

ようするに

 「192.168.254.21:9000へbind()したいのですが、まだそのコネクションがTIME_WAIT状態で残っているのでダメですよ」

ってこと、じゃSO_REUSEADDRをつけるとどうなるのと言えば...

* SO_REUSEADDRを有効にした場合

「和人! Socketをリユーズするんだ!」でソケットにオプションを追加しています
~ $ ./reuse
和人は創りました(1号) pid=5966
和人! Socketをリユーズするんだ! <----- SO_REUSEADDRをいれる
和人の子 pid=5966 は暇人なので 6秒だけ寝るw..
和人は創りました(2号) pid=5967
和人の子 pid=5967 は暇人なので 3秒だけ寝るw..
和人は創りました(3号) pid=5969
和人の子 pid=5969 は暇人なので 7秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58689
和人の子供がオワタ(3号) 5967
和人は創りました(3号) pid=5970
...
...
和人の子供がオワタ(4号) 5969
和人は創りました(4号) pid=5977
和人の子 pid=5977 は暇人なので 8秒だけ寝るw..
和人は創りました(5号) pid=5994
和人の子 pid=5994 は暇人なので 8秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58694
和人の子供がオワタ(5号) 5973
和人は創りました(5号) pid=5996
和人の子 pid=5996 は暇人なので 4秒だけ寝るw..
割り込み
~ $ ./reuse
和人は創りました(1号) pid=5999
和人! Socketをリユーズするんだ!
和人の子 pid=5999 は暇人なので 5秒だけ寝るw..
和人は創りました(2号) pid=6001
和人の子 pid=6001 は暇人なので 8秒だけ寝るw..
和人は創りました(3号) pid=6002
和人の子 pid=6002 は暇人なので 8秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58695
和人の子供がオワタ(3号) 5999
和人は創りました(3号) pid=6003
...
...
和人の子 pid=6028 は暇人なので 3秒だけ寝るw..
和人が親なんて信じられるかっ!! もう出るわ!
接続してきた和人の子 ... 192.168.254.21, 和人ポート 58702
和人の子供がオワタ(4号) 6026
和人は創りました(4号) pid=6029
和人の子 pid=6029 は暇人なので 3秒だけ寝るw..
~ $ ./reuse
和人! Socketをリユーズするんだ!
和人は創りました(1号) pid=6033
和人の子 pid=6033 は暇人なので 9秒だけ寝るw..
和人は創りました(2号) pid=6034
和人の子 pid=6034 は暇人なので 7秒だけ寝るw..
和人は創りました(3号) pid=6036
和人は創りました(4号) pid=6037
...
...

~ $ netstat -tpn
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 192.168.254.21:58698      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58700      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58704      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58699      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58703      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:9000       192.168.254.21:58702      TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58701      192.168.254.21:9000       TIME_WAIT   -                   
tcp        0      0 192.168.254.21:58702      192.168.254.21:9000       TIME_WAIT   -    
TIME_WAITが残ってても、必ずbind()が成功する。

サーバーを再起動とかかけたときにSO_REUSEADDRをしてないと、コネクションが綺麗に始末されるまでサーバーを起動できないって事になりかねませんね、これは

危ない!

ということで、サーバーを書くときはこのオプションをつけましょうというお話、

まぁ和人はどうでもいいが...

もう、和人のSocketネタはつまらないからやめる...

最後にコード


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/* Solaris: cc -g -O0 so_reuseaddr.c -lsocket -lnsl -o no_reuse */
/*          cc -g -O0 so_reuseaddr.c -lsocket -lnsl -DKAZUTO -o reuse */

/* Linux:   gcc -g -O0 so_reuseaddr.c -o no_reuse */
/*          gcc -g -O0 so_reuseaddr.c -DKAZUTO -o reuse */
int process = 0;

void sig_child(int);
void child_func(void);
void server_func(void);
void set_sockaddrin(struct sockaddr_in *, int);

int main(void)
{
  pid_t pid;

  struct sigaction sa, osa;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = 0;
  sa.sa_handler = &sig_child;
  sa.sa_flags |= SA_RESTART;
  sigaction(SIGCHLD, &sa, &osa);

  pid = fork();
  if(pid == 0) {
    server_func();
  } else if(pid < 0) {
    perror("しょっぱなから無理でしょ");
    return -1;
  }

  while(1) {
    if(process < 5) {
      pid = fork();
      if(pid == 0) {
        child_func();
      } else if(pid < 0) {
        perror("和人は小作りに失敗しました");
      } else {
        process++;
        printf("和人は創りました(%d号) pid=%d\n", process, pid);
      }
    }
    sleep(2);
  }
  return 1;
}

void server_func(void)
{
  int sock, result, con;
  socklen_t len;
  struct sockaddr_in serv, cli;
  char buff[1024];
  const char *p;
#ifdef KAZUTO
  /* SO_REUSEADDRを有効にする場合は1、無効にする場合は0をセット */
  int reuseaddr = 1;
#endif

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if(sock == -1) {
    perror("ソケット...どうしてくれんだ");
    exit(EXIT_FAILURE);
  }
  set_sockaddrin(&serv, sizeof(serv));

#ifdef KAZUTO
  puts("和人! Socketをリユーズするんだ!");
  result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
        (const void *) &reuseaddr, sizeof(int));
  if(result == -1) {
    perror("和人はできまい、、がセットできないよぉー\n");
    exit(EXIT_FAILURE);
  }
#endif

  if (bind(sock, (struct sockaddr*)&serv, sizeof(serv)) == -1) {
    perror("ああぁー、やっちまった...");
    exit(EXIT_FAILURE);
  }

  if(listen(sock, 15) < 0) {
    perror("ああぁー、リッスンゴレライ...");
    exit(EXIT_FAILURE);
  }
  
  while(1) {
    len = sizeof(cli);
    con = accept(sock, (struct sockaddr*)&cli, &len);
    if(con < 0) {
      perror("なにが起こった和人くん!");
    } else {
      p = inet_ntop(AF_INET, &cli.sin_addr, buff, sizeof(buff));
      printf("接続してきた和人の子 ... %s, 和人ポート %d\n",
             p,
             ntohs(cli.sin_port));
      close(con);
    }
  }
}

void child_func(void)
{
  int sock;
  int r, result;
  time_t t;
  pid_t pid;
  struct sockaddr_in serv;
  
  pid = getpid();

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if(sock == -1) {
    perror("ソケット...そそそ");
    exit(EXIT_FAILURE);
  }
  set_sockaddrin(&serv, sizeof(serv));

  r = 0;
  while(r < 3) {
    t = time(NULL); 
    srand(t);
    r = rand() % 10;
  }

  printf("和人の子 pid=%d は暇人なので %d秒だけ寝るw..\n", pid, r);
  sleep(r);

  if(connect(sock, (struct sockaddr*)&serv, sizeof(serv)) < 0) {
    perror("和人め裏切ったか...");
    exit(1);
  }

  puts("和人が親なんて信じられるかっ!! もう出るわ!");
  close(sock);
  exit(1);
}

void sig_child(int no)
{
  pid_t pid;
  int stat;
  while((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
    printf("和人の子供がオワタ(%d号) %d\n", process, pid);
    if(process > 0) {
      process--;
    }
  }
  return;
}

void set_sockaddrin(struct sockaddr_in *serv, int size)
{
  bzero(serv, size);
  serv->sin_family = AF_INET;
  inet_pton(AF_INET, "192.168.254.21", &serv->sin_addr.s_addr);
  serv->sin_port = htons(9000);
}


Socket遊びは止めるんだ... 和人...


0 件のコメント:

コメントを投稿