| 前へ << 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 までお願いします。