複数の接続を処理できるサーバーの書き方
telnetを同時に起動して試してみれば分かりますが、上のサーバーでは複数の接続を同時に捌くことはできません。
while(accept)のループで一つの接続のみを処理していて、そこを出るまでは次の接続を処理することができないからです。
自鯖のHTTPd位ならともかく、チャットやオンラインゲームなど、根本的に複数人が接続し続けるタイプのサービスは、そのままでは作ることができません。
それに対処するためには、いくつか方法があります。
- ポーリングする(select,poll,epoll,kqueue等)
- 接続を子プロセスに投げる(fork)
- 接続をスレッドに投げる(threads)
2,3も有用ですが、適当に作るとメモリを食ってしまい、まじめに作ると多少面倒なので、とりあえず1のポーリングでの対処を説明します。
これは、新しいプロセスやスレッドを作らず、現在のプロセス一つで複数の接続を扱う方法です。
ソケットを用いた双方向の通信は、ローカルの処理速度と比べるとスカスカです。なので、ソケット1に送信し、レスポンスを待つ間にソケット2にも送信、さらにソケット3から受信して、ソケット1から受信と言う風に、スカスカの間隔を上手く処理すれば、複数の通信を同時にこなすことができます。
これには、オーソドックスにはselectというシステムコールを使います。(ただしこれは遅いので、接続者が増えてきたらepoll/kqueueを用います。これは後述)
while(select)で送受信の準備ができたソケット(というかハンドル)を調べて、順次処理します。こいつらを処理してる間に他のソケットが準備できて、次のループが回るという寸法です。
select関数を生で扱うと大変面倒なので、IO::Selectを用いたサンプルコードを書きます。
ついでにSocketもIO::Socketを用います。この手のモジュールは是非活用しましょう
use IO::Select; use IO::Socket; #さっきのbindやらlistenやらを、下の一行でかけます my $listener = new IO::Socket::INET(Listen => 1, LocalPort => 6666, ReuseAddr => 1); #selectorにリスナーソケットを追加。あとで更に、クライアントのソケットも投げ込みます。 my $selector = new IO::Select( $listener ); #読み込み可能なハンドル@readyを入手。あるまでブロック while(my @ready = $selector->can_read) { foreach my $fh (@ready) { if($fh == $listener) { # 新規接続を受け付け、selectorに追加 my $new = $listener->accept; $selector->add($new); } else { # 入力を処理する。切断もここで処理 my $input = <$fh>; if(defined($input)){ print $fh $input; } else{ $selector->remove($fh); $fh->close; } } } }
telnetl localhost 6666を2,3個立ち上げて試してみてください。同時に処理できているはずです。
これはechoサーバーの例ですが、$inputを処理するあたりを書き換えるだけで、どんなサーバーにでもなります。
ね?簡単でしょ?