A mere hateblo

単なるはてブロです。

複数のクライアントから接続を受ける際のselect(2)の動作を確認した

ソケットを使ったサーバー・クライアントを実装するとして、サーバーに複数のクライアントから接続を求められた場合、selectがどのような動作をするのか確かめたくなった。まずはサーバーから:

#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>

static volatile sig_atomic_t loop_flag = !0;

void sigint_handler(int signum);

void sigint_handler(int signum)
{
  (void)signum;
  loop_flag = 0;
}

int main()
{
  int ret;
  int server_sock;
  int flag;
  struct sockaddr_in server_addr;
  fd_set fds;
  struct timeval tv;
  int num_conn;
  struct sockaddr_in client_addr;
  socklen_t client_addr_len;
  int client_sock;

  if (signal(SIGINT, sigint_handler) == SIG_ERR) {
    perror("signal");
    return 9;
  }

  // ソケットを生成する。
  ret = socket(PF_INET, SOCK_STREAM, 0);
  if (ret == -1) {
    perror("socket");
    return 1;
  }
  server_sock = ret;
  printf("server_sock = %d\n", server_sock);

  // ソケットでの通信でブロックしないように設定する。
  ret = fcntl(server_sock, F_GETFL, 0);
  if (ret == -1) {
    perror("fcntl");
    if (close(server_sock) == -1) {
      perror("close");
    }
    return 2;
  }
  flag = ret | O_NONBLOCK;
  ret = fcntl(server_sock, F_SETFL, flag);
  if (ret == -1) {
    perror("fcntl");
    if (close(server_sock) == -1) {
      perror("close");
    }
    return 3;
  }

  // ソケットにアドレスを割り当てる。
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(1234);
  if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof server_addr) == -1) {
    perror("bind");
    if (close(server_sock) == -1) {
      perror("close");
    }
    return 4;
  }

  // ソケットを接続待ちソケットとする。
  if (listen(server_sock, 5) == -1) {
    perror("listen");
    if (close(server_sock) == -1) {
      perror("close");
    }
    return 5;
  }

  while (loop_flag) {
    do {
      // クライアントからの接続を待つ。
      FD_ZERO(&fds);
      FD_SET(server_sock, &fds);
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      ret = select(server_sock + 1, &fds, NULL, NULL, &tv);
      if (ret == -1) {
        perror("select");
        if (close(server_sock) == -1) {
          perror("close");
        }
        return 6;
      }
      num_conn = ret;

      for (int i = 0; i < num_conn; ++i) {
        // クライアントからの接続を受ける。
        ret = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
        if (ret == -1) {
          perror("accept");
          if (close(server_sock) == -1) {
            perror("close");
          }
          return 7;
        }
        client_sock = ret;
        printf("%d/%d: client_sock = %d\n", i + 1, num_conn, client_sock);

        // 接続をクローズする。
        if (close(client_sock) == -1) {
          perror("close");
          return 8;
        }
        printf("close client socket %d\n", client_sock);
      }
    } while (num_conn > 0);

    fputc('.', stdout);
    fflush(stdout);
    sleep(1);
  }

  // ソケットをクローズする。
  if (close(server_sock) == -1) {
    perror("close");
  }

  printf("done.\n");
  return 0;
}

上記コードを書いた時点で力尽きそうになったので、クライアントはRubyで書いた:

require 'socket'

addrs = []
3.times do
  addrs << Addrinfo.tcp('127.0.0.1', 1234)
end

addrs.each do |addr|
  addr.connect
end

ついでにbuild.ninjaも:

builddir = build
cc = gcc
cflags = -Wall -Wextra -O0 -g
cppflags =
ld = $cc
ldflags =
libs =

rule cc
  description = CXX $out
  command = $cc -MMD -MT $out -MF $out.d $cflags $cppflags -o $out -c $in
  depfile = $out.d
  deps = gcc
rule link
  description = LINK $out
  command = $ld $ldflags -o $out $in $libs

build $builddir/s.o: cc s.c
build s: link $builddir/s.o

サーバー側のコードを実装しながら、自分は「クライアントが一度に3つ接続しようとするのだから、select(2)が3を返して、accept(2)を3回連続して呼び出せる」ような挙動をするのかなあなどと考えていた。

とりあえずサーバーを起動して:

% ./s
server_sock = 3
...........

つぎに別の端末から3秒後にクライアントを起動するように仕込む:

% sleep 3 ; ruby c.rb

元の端末に戻ってクライアントが起動するのを待つ。すると、select(1)が1を3回返すという結果になった:

% ./s
server_sock = 3
.......................................................1/1: client_sock = 4
close client socket 4
1/1: client_sock = 4
close client socket 4
1/1: client_sock = 4
close client socket 4
.........................^Cdone.

結局のところselect(2)が返す値は引数で指定したソケットの数になるようで、同じソケットで複数の接続を受けるなら複数回select(2)する必要がある、ということかな。

ということは、結局サーバーがソケットを一つしか用意せず、一度に一つしかクライアントを受けられないのであれば、せっかくノンブロックングモードにしてあるのだからselect(2)を使うまでもなくaccept(2)を直接使えば良いのかもしれない。この辺りは後日確認しよう。

理解が浅いままだけど挙動は確認できたので満足。