複数のクライアントから接続を受ける際の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)を直接使えば良いのかもしれない。この辺りは後日確認しよう。
理解が浅いままだけど挙動は確認できたので満足。