前へ << echo サーバを作ってみよう (3) | FTP クライアントを作ってみよう (1) >> 次へ |
前節では、fork を使ったマルチスレッド版 echo サーバを作りました。 一つ一つのプロセスは単純な動作しかしていないので、作るのも 理解するのも簡単なのですが、欠点は「プロセス数が増加しすぎること」です。
クライアントが接続していない状態でも 1 プロセス、 5 クライアントが接続していると 6 つものプロセスが実行されることになります。
今度は select を使って、全てを 1 つのプロセスの中で処理することにしましょう。 ここでいう select とは 4引数 select のことです。 perl には 1引数の select と 4引数の select があります。 1引数 select はデフォルトのファイルハンドルを選択する関数ですが、 4引数 select とは全く関係ありません。
まず、 C言語のファイル入出力関数を使おう を読んで、ファイルの入出力はファイルディスクリプタという番号で 制御されることを理解して下さい。 perl では <STDIN> とか <CLIENT> などのファイルハンドルで ファイルやソケットとの入出力ができますが、perl の内部では やはりファイルディスクリプタという番号で処理されています。
perl でファイルディスクリプタの番号を調べるには、fileno という関数を使います。
#!/usr/local/bin/perl printf("fileno(STDIN)=%d\n", fileno(STDIN)); printf("fileno(STDOUT)=%d\n", fileno(STDOUT)); printf("fileno(STDERR)=%d\n", fileno(STDERR)); open(IN, "a"); # 常にファイルのオープンに成功するものと仮定します printf("fileno(IN)=%d\n", fileno(IN)); open(OUT, ">b"); # 常にファイルのオープンに成功するものと仮定します printf("fileno(OUT)=%d\n", fileno(OUT));実行結果は
fileno(STDIN)=0 fileno(STDOUT)=1 fileno(STDERR)=2 fileno(IN)=3 fileno(OUT)=4となります。同様にソケットについても fileno の結果を見てみましょう。
use Socket; socket(SOCKET, PF_INET, SOCK_STREAM, 0); printf("fileno(SOCKET)=%d\n", fileno(SOCKET));結果は
fileno(SOCKET)=5となります。つまり、ソケットもファイルハンドルも、 ファイルディスクリプタという番号で識別されているわけです。 C言語のファイル入出力関数を使おう・ C言語でHTTPクライアントを作ってみよう を見ていただけるとわかりますが、
$in = <SOCKET>;とすると、ソケットにデータが入っている場合は、その内容が $in に代入されますが、もしソケットにデータがない場合はブロックされてしまいます。 つまり、データが送られてくるまで永久に他の仕事ができなくなるのです。 そこで、select を使ってソケットから読み込む前に「ソケットに読み込むべきデータがあるかどうか」 を調べることにしましょう。select は
select($read_bits, undef, undef, $timeout)という書式を取ります。 $read_bits には、調べたいディスクリプタの情報を事前にセットしておきます。 具体的には、
$ret = select($read_bits, undef, undef, $timeout);を実行すると、OS は $read_bits の内容を読んで、指定されたディスクリプタに 読み込むべきデータがあるかどうかを調べます。 そして読み込むべきデータがあるものだけ $read_bits のビット値を 1 にします (select は $read_bits の値を書き換えます)。
つまり、$read_bits を `00111000' として
$ret = select($read_bits, undef, undef, $timeout);を実行したとき、select によって書き換えられた $read_bits が
$timeout には、タイムアウトまでの秒数を設定します。 $timeout=5 ならば、5秒待ってもどのディスクリプタも入力可能にならなかった場合は select から戻ります。5秒の間に、あるディスクリプタから読み出し可能になると、 即座に (5秒待たずに) select から戻ります。
vec($var, $bit, $len)は、変数 $var の $bit 目から $len ビットを取り出します。 $var が `00110111' のとき、
vec($var,0,3) は `111' vec($var,2,2) は `01' vec($var,3,1) は `0'となります。ビットを設定するには、vec を左辺値として使います。 $var が `00000000' のとき、
vec($var,0,1)=1 とすると $var は `00000001' vec($var,1,1)=1 とすると $var は `00000010' vec($var,2,1)=1 とすると $var は `00000100'となります。
1: #!/usr/local/bin/perl -w 2: 3: # $Id: echo-server-select.pl,v 1.2 2002/02/05 17:53:08 68user Exp $ 4: 5: use Socket; # Socket モジュールを使う 6: 7: $port = 5000; 8: # ソケット生成 9: socket(CLIENT_WAITING, PF_INET, SOCK_STREAM, 0) 10: || die "ソケットを生成できません。$!"; 11: 12: # ソケットオプション設定 13: setsockopt(CLIENT_WAITING, SOL_SOCKET, SO_REUSEADDR, 1) 14: || die "setsockopt に失敗しました。$!"; 15: 16: # ソケットにアドレス(=名前)を割り付ける 17: bind(CLIENT_WAITING, pack_sockaddr_in($port, INADDR_ANY)) 18: || die "bind に失敗しました。$!"; 19: 20: # ポートを見張る 21: listen(CLIENT_WAITING, SOMAXCONN) 22: || die "listen: $!"; 23: 24: print "ポート $port を見張ります。\n"; 25: 26: %data_sockets = (); # 現在有効なデータコネクション用のハッシュテーブル 27: $client_num = 0; # クライアントの通し番号 28: 29: $rin = &set_bits(CLIENT_WAITING); # ビット列を生成 30: 31: while (1){ 32: $ret = select($rout=$rin, undef, undef, undef); 33: printf("\$ret=$ret \$rout=%s,\$rin=%s\n", &to_bin($rout), &to_bin($rin)); 34: 35: if ( vec($rout, fileno(CLIENT_WAITING), 1) ){ # 新たにクライアントがやってきた 36: 37: # ソケット名は毎回違う名前にする 38: $new_socket = "CLIENT_$client_num"; 39: $sockaddr = accept($new_socket, CLIENT_WAITING); 40: 41: # ホスト名、IPアドレス、クライアントのポート番号を取得 42: ($client_port, $client_iaddr) = unpack_sockaddr_in($sockaddr); 43: $client_hostname = gethostbyaddr($client_iaddr, AF_INET); 44: $client_ip = inet_ntoa($client_iaddr); 45: 46: print "接続: $client_hostname ($client_ip) ポート $client_port\n"; 47: print "ソケット $new_socket を生成します。\n"; 48: 49: # クライアントに対してバッファリングしない 50: select($new_socket); $|=1; select(STDOUT); 51: 52: # 接続中のクライアントをテーブルに登録 53: $data_sockets{$new_socket} = 1; 54: 55: # select に渡すビット列を更新 56: $rin = &set_bits(CLIENT_WAITING, keys %data_sockets); 57: $client_num++; 58: 59: } elsif ( $ret ){ # 接続中のクライアントから、データが送信されてきた 60: 61: foreach $sock ( sort keys %data_sockets ){ # どのクライアントかを一つずつ確かめる 62: print " check... $sock\n"; 63: if ( vec($rout, fileno($sock), 1) ){ 64: if ( $in = <$sock> ){ # 1行読んでそのまま返す 65: print " $sock からの入力 .. $in"; 66: print $sock "$in"; 67: } else { # エラー発生=コネクション切断 68: print " コネクション切断 $sock\n"; 69: close($sock); # ファイルハンドルを close 70: delete $data_sockets{$sock}; # テーブルから削除 71: # select に渡すビット列を更新 72: $rin = &set_bits(CLIENT_WAITING, keys %data_sockets); 73: } 74: } 75: } 76: } 77: } 78: 79: 80: #---------------------------------------------------- 81: # 1個以上のファイルハンドルを受け取り、fileno で各ファイルハンドルの 82: # ディスクリプタ番号を調べ、それに対応するビットを立てたデータを返す。 83: # 例えば 84: # fileno(CLIENT_WAITING)==3 85: # fileno(CLIENT_1) ==4 86: # fileno(CLIENT_3) ==6 87: # のとき、 88: # &set_bits(CLIENT_WAITING, CLIENT_1, CLIENT_3) 89: # は 90: # 01011000 91: # というデータを返す。 92: 93: sub set_bits { 94: @sockets = @_; 95: 96: print "select に渡すビット列 \$rin を生成します。\n"; 97: $rin=""; 98: foreach $sock (@sockets){ 99: # $rin の、右から数えて fileno($sock) 番目のビットを1にする。 100: vec($rin, fileno($sock), 1)=1; 101: printf(" fileno($sock) は %d なので \$rin は %s になります。\n", 102: fileno($sock), 103: &to_bin($rin), 104: ); 105: } 106: return $rin; 107: } 108: 109: 110: #---------------------------------------------------- 111: # 引数を受け取り、2進数の文字列(010111...)に変換して返す。 112: 113: sub to_bin { 114: return unpack "B*", $_[0]; 115: }まず、おおまかな流れを説明します。
% ./echo-server-select.pl ポート 5000 を見張ります。 select に渡すビット列 $rin を生成します。 fileno(CLIENT_WAITING) は 4 なので $rin は 00010000 になります。これは
24: print "ポート $port を見張ります。\n"; 25: 26: %data_sockets = (); # 現在有効なデータコネクション用のハッシュテーブル 27: $client_num = 0; # クライアントの通し番号 28: 29: $rin = &set_bits(CLIENT_WAITING); # ビット列を生成 30: 31: while (1){ 32: $ret = select($rout=$rin, undef, undef, undef);の部分が実行されたわけです。
ポート監視用ソケット CLIENT_WAITING のファイルディスクリプタは 4番だったので、&set_bits によって $rin の 4ビット目 (右端が0番目) が 1にセットされました。 4 引数 select のタイムアウトとして undef を指定したので、 ここで実行は止まっています。クライアントが接続してくるまで このままブロックし続けるのです。
% telnet localhost 5000として、ポート 5000 に接続して下さい。 すると echo サーバは
$ret=1 $rout=00010000,$rin=00010000 接続: localhost (127.0.0.1) ポート 57603 ソケット CLIENT_0 を生成します。というデバッグ情報を出力します。この部分に対応するコードは
33: printf("\$ret=$ret \$rout=%s,\$rin=%s\n", &to_bin($rout), &to_bin($rin)); 34: 35: if ( vec($rout, fileno(CLIENT_WAITING), 1) ){ # 新たにクライアントがやってきた 36: 37: # ソケット名は毎回違う名前にする 38: $new_socket = "CLIENT_$client_num"; 39: $sockaddr = accept($new_socket, CLIENT_WAITING); 40: 41: # ホスト名、IPアドレス、クライアントのポート番号を取得 42: ($client_port, $client_iaddr) = unpack_sockaddr_in($sockaddr); 43: $client_hostname = gethostbyaddr($client_iaddr, AF_INET); 44: $client_ip = inet_ntoa($client_iaddr); 45: 46: print "接続: $client_hostname ($client_ip) ポート $client_port\n"; 47: print "ソケット $new_socket を生成します。\n"; 48: 49: # クライアントに対してバッファリングしない 50: select($new_socket); $|=1; select(STDOUT); 51: 52: # 接続中のクライアントをテーブルに登録 53: $data_sockets{$new_socket} = 1;です。$rout が `00010000' となっています。つまり 「ファイルディスクリプタ 4 番に入力があった」ということです。 ソケット監視用ポートへの入力があるということは、新たなクライアントがやってきたということです。 デバッグ情報により、クライアントは localhost(127.0.0.1) のポート 57603 から 接続していることがわかります。
そしてクライアントと通信するためのソケットを生成します。 このクライアントとのデータ通信用ソケットは `CLIENT_0' です。 現在有効なデータ通信用ソケットを記録しておくため、 ハッシュ %data_sockets にソケット名を登録します。
さて、この時点でのソケットは CLIENT_WAITING (ポート監視用ソケット) と CLIENT_0 (クライアントとの通信用) の 2つです。 2 つのソケットに対して読み出し可能かどうかを調べるため、$rin を更新します。 その部分が
55: # select に渡すビット列を更新 56: $rin = &set_bits(CLIENT_WAITING, keys %data_sockets); 57: $client_num++;です。これに対応するデバッグ出力は
select に渡すビット列 $rin を生成します。 fileno(CLIENT_WAITING) は 4 なので $rin は 00010000 になります。 fileno(CLIENT_0) は 5 なので $rin は 00110000 になります。になります。4ビット目と5ビット目 (右端が0ビット目) を 1 にしたので、 ファイルディスクリプタ 4 番 (CLIENT_WAITING) と 5番 (CLIENT_0) が 読み出し可能かどうか調べることになります。
接続: localhost (127.0.0.1) ポート 57631 ソケット CLIENT_1 を生成します。 select に渡すビット列 $rin を生成します。 fileno(CLIENT_WAITING) は 4 なので $rin は 00010000 になります。 fileno(CLIENT_0) は 5 なので $rin は 00110000 になります。 fileno(CLIENT_1) は 6 なので $rin は 01110000 になります。となります。1つクライアントが増え、ソケット CLIENT_1 が 生成されました。CLIENT_1 のファイルディスクリプタ番号は 6 なので、 それに対応する $rin の 6 ビット目が 1 になります。
では 2つの telnet 上で適当に文字をタイプして下さい。 最初に実行した telnet 上で abc とタイプすると、 echo-server-select.pl は
$ret=1 $rout=00100000,$rin=01110000 check... CLIENT_0 CLIENT_0 からの入力 .. abc check... CLIENT_1と出力します。後から実行した telnet 上で def とタイプすると
$ret=1 $rout=01000000,$rin=01110000 check... CLIENT_0 check... CLIENT_1 CLIENT_1 からの入力 .. defとなります。1つのプロセスでマルチスレッドサーバが実現できていることを確認して下さい。
クライアントからデータが送らると、ソケットから読み出し可能になるのですが、
32: $ret = select($rout=$rin, undef, undef, undef);select から処理が戻ってきた時点では、クライアントからデータが送られてきたのか、 それとも新たなクライアントが接続してきたのかわかりません。しかし、
35: if ( vec($rout, fileno(CLIENT_WAITING), 1) ){ # 新たにクライアントがやってきたで、CLIENT_WAITING に対応する 4 ビット目は 1 ではないことがわかるので、
59: } elsif ( $ret ){ # 接続中のクライアントから、データが送信されてきたに処理が行きます。
61: foreach $sock ( sort keys %data_sockets ){ # どのクライアントかを一つずつ確かめる 62: print " check... $sock\n"; 63: if ( vec($rout, fileno($sock), 1) ){ 64: if ( $in = <$sock> ){ # 1行読んでそのまま返す 65: print " $sock からの入力 .. $in"; 66: print $sock "$in"; 67: } else { # エラー発生=コネクション切断 68: print " コネクション切断 $sock\n"; 69: close($sock); # ファイルハンドルを close 70: delete $data_sockets{$sock}; # テーブルから削除 71: # select に渡すビット列を更新 72: $rin = &set_bits(CLIENT_WAITING, keys %data_sockets); 73: } 74: } 75: }ここでは、ハッシュ %data_sockets から一つずつソケット名を $sock に 代入し、どのソケットに対応するビットが 1 になっているかを
63: if ( vec($rout, fileno($sock), 1) ){で調べます。この if 文が真になったら、そのソケットは読み出し可能である、ということです。 しかし、コネクションが切断された場合も考慮しなくてはいけません。 実行中の telnet を
^] (Ctrl-]を入力) telnet> quit Connection closed.と、強制的に終了すると、そのコネクションは切断されますが、 この場合も $rout の対応するビットが 1 になるのです。 ソケットからデータが送られてきたのか、それともコネクションが切断されたのかは、
64: if ( $in = <$sock> ){ # 1行読んでそのまま返す 65: print " $sock からの入力 .. $in"; 66: print $sock "$in"; 67: } else { # エラー発生=コネクション切断 68: print " コネクション切断 $sock\n"; 69: close($sock); # ファイルハンドルを close 70: delete $data_sockets{$sock}; # テーブルから削除 71: # select に渡すビット列を更新 72: $rin = &set_bits(CLIENT_WAITING, keys %data_sockets); 73: }というふうに、ソケットからの読み出し $in = <sock> がエラーになったか どうかで判別できます。 エラーにならずに正常に読み込めたら、ソケットにそのまま内容を返します (echo サーバだから)。 エラーになったらコネクションが切断されたということなので、 ソケットを close し、ハッシュ %data_sockets からファイルハンドルを 削除、select に渡す $rin を更新します。
32: $ret = select($rout=$rin, undef, undef, undef);の行を
$ret = select($rout=$rin, undef, undef, 1);に変えてみて下さい (最後の undef を 1 にする)。
すると、1秒待ってもソケットが読み出し可能にならないときは select から処理が戻り、時々刻々と $rin・$rout の 値を出力します。
telnet で接続したり、文字列を送信するたびに $rin や $rout の値が変わっていくのがわかると思います。
前へ << echo サーバを作ってみよう (3) | FTP クライアントを作ってみよう (1) >> 次へ |
ご意見・ご指摘は Twitter: @68user までお願いします。