前へ << echo サーバを作ってみよう (2) | echo サーバを作ってみよう (4) >> 次へ |
まぁこれは問題点というか、仕様であると言ってしまえば それでおしまいなのかもしれません。でもやはり同時に 複数のクライアントの相手をできる方が賢いでしょう。 今回はこの点を改善してみます。
最初に実行されていたサーバ (親プロセス) はポートを見張り続けます。 クライアントから接続があった場合は子プロセスを生成し、 その後のクライアント相手は子プロセスにまかせることにします。 親プロセスは子プロセスを生成した後は、クライアントとは 関わりあわず、引続きポートを見張ることに専念します。
このように、同時に複数のクライアントが接続できるようなサーバを、 マルチスレッドサーバといいます。
1: #!/usr/local/bin/perl -w 2: 3: # $Id: echo-server-fork.pl,v 1.2 2002/02/17 11:07:27 68user Exp $ 4: 5: use Socket; 6: $port = 5000; 7: # ソケット生成 8: socket(CLIENT_WAITING, PF_INET, SOCK_STREAM, 0) 9: or die "ソケットを生成できません。$!"; 10: 11: # ソケットオプション設定 12: setsockopt(CLIENT_WAITING, SOL_SOCKET, SO_REUSEADDR, 1) 13: or die "setsockopt に失敗しました。$!"; 14: 15: # ソケットにアドレス(=名前)を割り付ける 16: bind(CLIENT_WAITING, pack_sockaddr_in($port, INADDR_ANY)) 17: or die "bind に失敗しました。$!"; 18: 19: # ポートを見張る 20: listen(CLIENT_WAITING, SOMAXCONN) 21: or die "listen: $!"; 22: 23: print "親プロセス($$): ポート $port を見張ります。\n"; 24: 25: # while(1)することで、1つの接続が終っても次の接続に備える 26: while (1){ 27: $paddr = accept(CLIENT, CLIENT_WAITING); 28: 29: # ホスト名、IPアドレス、クライアントのポート番号を取得 30: ($client_port, $client_iaddr) = unpack_sockaddr_in($paddr); 31: $client_hostname = gethostbyaddr($client_iaddr, AF_INET); 32: $client_ip = inet_ntoa($client_iaddr); 33: 34: print "接続: $client_hostname ($client_ip) ポート $client_port\n"; 35: 36: # forkで子プロセスを生成 37: if ( $pid = fork() ){ 38: # こちらは親プロセス 39: print "親プロセス($$): 引続きポート $port を見張ります。\n"; 40: print "親プロセス($$): クライアントの相手はプロセス $pid が行います。\n"; 41: 42: # 親プロセスはソケットをクローズ 43: close(CLIENT); 44: next; 45: } else { 46: # こっちは子プロセス 47: 48: # クライアントに対してバッファリングしない 49: select(CLIENT); $|=1; select(STDOUT); 50: while (<CLIENT>){ 51: print "子プロセス($$): メッセージ $_"; 52: # クライアントにメッセージを返す 53: print CLIENT $_; 54: } 55: close(CLIENT); 56: print "子プロセス($$): 接続が切れました。終了します。\n"; 57: # ポートの監視は親プロセスが行っているので、 58: # クライアントとのやりとりが終了すれば exit 59: exit; 60: } 61: }前のバージョンと違うところは、
37: if ( $pid = fork() ){ 38: # こちらは親プロセス 39: print "親プロセス($$): 引続きポート $port を見張ります。\n"; 40: print "親プロセス($$): クライアントの相手はプロセス $pid が行います。\n"; 41: 42: # 親プロセスはソケットをクローズ 43: close(CLIENT); 44: next; 45: } else { 46: # こっちは子プロセス 47: 48: # クライアントに対してバッファリングしない 49: select(CLIENT); $|=1; select(STDOUT); 50: while (<CLIENT>){ 51: print "子プロセス($$): メッセージ $_"; 52: # クライアントにメッセージを返す 53: print CLIENT $_; 54: } 55: close(CLIENT); 56: print "子プロセス($$): 接続が切れました。終了します。\n"; 57: # ポートの監視は親プロセスが行っているので、 58: # クライアントとのやりとりが終了すれば exit 59: exit; 60: }だけで、それより前の部分は全く同じです。少しずつみていきましょう。
37: if ( $pid = fork() ){さて、いきなりよくわからない方もいるかもしれません。 fork とは新しいプロセスを生成するシステムコールです。 UNIX では新しいプロセスを生成する方法は、 システムコール fork(2) を使うしかありません。
なお、プロセスを作成する方法は fork(2) のみ、というのは ほんとは嘘で、vfork(2)、rfork(2) というシステムコールもあります。
fork によるプロセスの生成は、(知らない人にとっては)とても特徴的です。 fork で作られる子プロセスは、親プロセス (fork を呼んだ側) の コピーなのです。ファイルハンドル、変数、環境変数など、 子プロセスには親プロセスと全く同じ状態が引き継がれます。
唯一違うのが fork の戻り値です。サンプルを見てもらうと話は 早いのですが、
if ( $pid = fork() ){ ここに処理がくると親プロセス } else { ここに処理がくると子プロセス }fork はプロセスの分身を作り出したあと、値を返します。 ここで親プロセスには「新しく作ったプロセスのプロセス番号 (=子プロセスのプロセス番号)」が返され、 子プロセスには「0」が返されます。fork の戻り値を調べないと、 自分自身が親プロセスなのか子プロセスなのかはわかりません。
もう少しわかりやすく書くと、
$pid = fork(); if ( $pid != 0 ){ ここに処理がくると親プロセス } else { ここに処理がくると子プロセス }というわけです。上に少し書いたとおり、ファイルハンドル、変数、環境変数などは 全て同じものが渡されます。これは子プロセスのメモリ領域に、親プロセスのメモリ領域のデータを まるごとコピーしたということです。
37: if ( $pid = fork() ){ 38: # こちらは親プロセス 39: print "親プロセス($$): 引続きポート $port を見張ります。\n"; 40: print "親プロセス($$): クライアントの相手はプロセス $pid が行います。\n"; 41: 42: # 親プロセスはソケットをクローズ 43: close(CLIENT); 44: next; 45: } else {とっても短いですね。まずメッセージを出力した後、 ソケット CLIENT を close します。クライアントとのやりとりは 子プロセスにまかせるのですから、もうクライアントとの 出入口であるソケットは必要ありません。親プロセスの担当は CLIENT_WAITING の方です。
27: $paddr = accept(CLIENT, CLIENT_WAITING);に移ります。親プロセスは子プロセスを生成したら、引続きポートを 監視し続けます。別のクライアントが接続してこないと、 ここで動作は止まります (ブロックしている)。 新たなクライアントがやってくると accept から戻り、再度 fork します。
45: } else { 46: # こっちは子プロセス 47: 48: # クライアントに対してバッファリングしない 49: select(CLIENT); $|=1; select(STDOUT); 50: while (<CLIENT>){ 51: print "子プロセス($$): メッセージ $_"; 52: # クライアントにメッセージを返す 53: print CLIENT $_; 54: } 55: close(CLIENT); 56: print "子プロセス($$): 接続が切れました。終了します。\n"; 57: # ポートの監視は親プロセスが行っているので、 58: # クライアントとのやりとりが終了すれば exit 59: exit; 60: }やっていることは、ほとんど前バージョンと違いはありませんが、大事なのは最後の
59: exit;です。ポートを見張る仕事は親プロセスが行うのですから、 子プロセスはクライアントとのやりとりが終了すれば exit で自分自身のプロセスを終了させます。もし exit が ないと、クライアントからの接続が終っても、意味のない プロセスが残ってしまいます。
この改良版を動かして、同時に複数の telnetで接続してみてください。 どの telnet でも、タイプした文字が即座に返ってくるはずです。
20: listen(CLIENT_WAITING, SOMAXCONN) 21: or die "listen: $!";
27: $paddr = accept(CLIENT, CLIENT_WAITING);listen というのは、OS に対して「ポートに接続してきたクライアントとの コネクションを確立しておいてね」と命令しているのです。
ここで思い出して欲しいのですが、非マルチスレッド版 echo サーバ (最初に作った echo サーバ)に対して複数の telnet で接続すると、 後から接続した方は、前のクライアントが終了するまで 待たされはしたものの、ちゃんとデータのやりとりはできていましたよね? 「今このクライアントの相手をしているから、あなたはダメ」なんて言われて 接続拒否、なんてことはありませんでした。
これは、クライアントが待たされていたときには、既にコネクションは 確立されていたからです。OS は一つのポートに同時に複数の 接続要求があると、片っ端からコネクションを確立していきます。 そして先に来たクライアントから順に、待ち行列に登録していきます。
しかし listen した時点では、サーバプロセスからはクライアントがやってきたことを 知ることはできません。コネクションの確立は、あくまでもOSが陰でやっていることなのです。
一方、accept というのは、待ち行列の先頭のクライアントと繋がった出入口(=ソケット)を 新しく生成し、そのクライアントを待ち行列から外すシステムコールです。 accept した時点で、初めてサーバプロセスはクライアントが待っていたことを 知ることができます。
イメージしにくい方のために、受け付け嬢の例え話をしましょう。
お客さんが担当の社員に会いに、会社にやってきます (connect)。
お客さんはまず受け付け嬢のところに向かいます。 受け付け嬢はお客さんを来客者名簿に登録し、 「担当の者が参りますので、しばらくお待ち下さい」 と言うのです。これが listen です。
担当社員が accept すると、お客さんとの話が始まります。 その間に新たなお客さんがやってくると、受け付け嬢のところで待たされます。 担当社員とお客さんの話が終わり、再び accept するまでお客さんは 受け付け嬢のところで待たなければいけません。
つまり、最初の echo サーバは、担当社員が1人しかいなかったので、 同時に複数のお客さんを相手にすることができなかったのです。 しかし、マルチスレッド版 echo サーバでは、担当社員が複数存在します。 複数の社員が accept することで、受け付け嬢のところでお客さんが 待たなくてもよい、というわけです。
20: listen(CLIENT_WAITING, SOMAXCONN)の SOMAXCONN について説明しましょう。ここに指定する値は backlog といって、 まだ accept されていないコネクション (OS が待たせているコネクション) の 最大数を指定するものです。もし backlog 以上のクライアントが同時に connect してきた場合は、何もレスポンスを返しません。
#define SOMAXCONN 128と定義されているので、
listen(CLIENT_WAITING, SOMAXCONN)と
listen(CLIENT_WAITING, 128)は同じ意味です。
ただし、注意しなければいけないのは、ここで指定した値がそのまま 使われないかもしれない、ということです。backlog の解釈は OS によって異なり、
なお、このスクリプトの中で SOMAXCONN という定数を設定しているのは、 Socket モジュールです。
ちなみに、FreeBSD 2.2.7-RELEASE では、listen すると 以下のカーネルソース部分に処理が渡ります。
@kern/uipc_syscalls.c (/usr/src/sys/kern/uipc_syscalls.c) int listen(p, uap, retval){ .... return (solisten((struct socket *)fp->f_data, uap->backlog)); }ここでは下請け関数 solisten の2番目の引数に backlog をそのまま渡しています。
@kern/uipc_socket.c int solisten(so, backlog){ ... if (backlog < 0 || backlog > somaxconn) backlog = somaxconn; so->so_qlimit = backlog; .... }solisten では backlog の値をチェックして 待ち行列の最大数を表す so_qlimit に代入します。
@kern/uipc_socket2.c struct socket *sonewconn1(head, connstatus){ if (head->so_qlen > 3 * head->so_qlimit / 2) return ((struct socket *)0);クライアントから接続されたときは、 まだ accept されていないコネクション数 so_qlen が so_qlimit の 1.5倍 (3分の2) より多ければ 何もせず return します。
SOMAXCONN は 128 なので、まだ accept していない コネクションは 192 個 (=128*1.5) 待たせておけるわけです。 なお、accept 済のコネクションはこの中に含まれません。
ああ、オープンソースって素晴らしい。
前へ << echo サーバを作ってみよう (2) | echo サーバを作ってみよう (4) >> 次へ |
ご意見・ご指摘は Twitter: @68user までお願いします。