echo サーバを作ってみよう (2)

前へ << echo サーバを作ってみよう (1) echo サーバを作ってみよう (3) >> 次へ

echoサーバ

これまで作ってきたものはクライアント、つまり接続する側でした。 今回はサーバを作るわけで、こちらは接続を待つ側です。 やはりそれなりに違う構成になります。

echo-server.pl

    1: #!/usr/local/bin/perl -w
    2: 
    3: # $Id: echo-server.pl,v 1.2 2002/02/17 11:07:27 68user Exp $
    4: 
    5: use Socket;
    6: 
    7: $port = 5000;
    8:                 # ソケット生成
    9: socket(CLIENT_WAITING, PF_INET, SOCK_STREAM, 0)
   10:      or die "ソケットを生成できません。$!";
   11: 
   12:                 # ソケットオプション設定
   13: setsockopt(CLIENT_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
   14:      or die "setsockopt に失敗しました。$!";
   15: 
   16:                 # ソケットにアドレス(=名前)を割り付ける
   17: bind(CLIENT_WAITING, pack_sockaddr_in($port, INADDR_ANY))
   18:      or die "bind に失敗しました。$!";
   19: 
   20:                 # ポートを見張る
   21: listen(CLIENT_WAITING, SOMAXCONN)
   22:      or die "listen: $!";
   23: 
   24: print "ポート $port を見張ります。\n";
   25: 
   26:                 # while(1)することで、1つの接続が終っても次の接続に備える
   27: while (1){
   28:     $paddr = accept(CLIENT, CLIENT_WAITING);
   29: 
   30:                 # ホスト名、IPアドレス、クライアントのポート番号を取得
   31:     ($client_port, $client_iaddr) = unpack_sockaddr_in($paddr);
   32:     $client_hostname = gethostbyaddr($client_iaddr, AF_INET);
   33:     $client_ip = inet_ntoa($client_iaddr);
   34: 
   35:     print "接続: $client_hostname ($client_ip) ポート $client_port\n";
   36: 
   37:                 # クライアントに対してバッファリングしない
   38:     select(CLIENT); $|=1; select(STDOUT);
   39: 
   40:     while (<CLIENT>){
   41:         print "メッセージ: $_";
   42:                 # クライアントにメッセージを返す
   43:         print CLIENT $_;
   44:     }
   45:     close(CLIENT);
   46: 
   47:     print "接続が切れました。引き続きポート $port を見張ります。\n";
   48: }

echoサーバの実行

早速実行してみましょう。
% ./echo-server.pl
ポート5000を見張ります。
ここで別のウィンドウから telnet でポート 5000 に接続し、 何か文字をタイプしてください。
% telnet localhost 5000
Trying 127.0.0.1...
flushoutput character is 'off'.
Connected to localhost.
Escape character is '^]'.
hoge
hoge
fuga
fuga
abcdefghijklmnopqrstuvexyz
abcdefghijklmnopqrstuvexyz
^] (Ctrlを押しながら ]を押す)
telnet > quit
Connection closed.
タイプした文字がそのままサーバから返ってきました。うまく echo サーバとして 機能しているようです。さてここでサーバの方をもう一度みると、
ポート5000を見張ります。
接続: localhost (127.0.0.1) ポート 1432
メッセージ: hoge
メッセージ: fuga
メッセージ: abcdefghijklmnopqrstuvexyz
接続が切れました。引き続きポート 5000 を見張ります。
となっているはずです。クライアントは localhost のポート 1432 から 接続してきたわけです。
ポートはサーバだけに割り当てられるものではありません。クライアント側にも ポートが割り当てられます (ポート番号何番が割り当てられるかは、OS によって決められます)。 ただ、クライアントは自分に割り当てられたポート番号を知ってもあまり意味がないですが。
ではプログラムを見ていきましょう。

listenまでの長い道のり

まず最初に、どのポートでクライアントを待つかを決めましょう。 UNIX では、ポート番号 1023 番以下を使うことのできるのは root のみです (root のみ bind できる)。 例えば、POP3 サーバが動いていないホストを考えてみましょう。 ポート 110 は空いていますが、もし一般ユーザがポート 110 を自由に使うことができたら、 POP3 サーバが動いていると勘違いした他のユーザが接続してくるかもしれません。 間違えて接続してきたクライアントはユーザ認証を試みますが、 その結果ユーザ名とパスワードという一番知られてはいけない情報が 他人の手に渡ってしまうのです。 このような事態を避けるために、well-known port が多い 1023 番以下のポートは、 root しか使えないようになっています。

というわけで、ポート 1024 番以上なら何番でもいいのですが、 キリのいいところで 5000 番にしましょう。

    7: $port = 5000;

サーバですから、どのホストのどのポートに接続するかということは 関係ありません。ただ待ってればいいんです。クライアントが行っていた inet_aton や pack_sockaddr_in はすっとばして、いきなりソケットを生成します。
    9: socket(CLIENT_WAITING, PF_INET, SOCK_STREAM, 0)
   10:      or die "ソケットを生成できません。$!";
→ 関数説明: socket

次の行では、ソケットに対してオプションを設定しています。
   13: setsockopt(CLIENT_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
   14:      or die "setsockopt に失敗しました。$!";
これは何かを説明するより、まず実験してみた方が理解しやすいでしょう。 試しにこの2行をコメントアウトしてサーバを起動し、 telnet で接続した後、サーバ側を Ctrl-c で終了させてください。 すると telnet も
Connection closed by foreign host.
となって終了します。ここですぐに もう一度サーバを起動してみると、
bindに失敗しました。Address already in use
となります。これは、サーバ側がいきなり接続を切ったため、 ポート番号 5000 に対するコネクションがまだシャットダウンしておらず、 再度サーバが bind しようとしても失敗するからです

なお、数秒〜数分待てばこの状態から回復し、bind できるようになります。 FreeBSD では数秒でしたが、数分待たされる OS もあるようです。

この問題を回避するのが SO_REUSEADDR です。ソケットに 対してこのオプションを設定すると、上記のような bind 時のエラーを回避できます。

実は、この説明では全く不十分です。

アクティブクローズした側 (能動的に接続を切った側。 この場合は echo サーバ) は TIME_WAIT という状態になります。 この TIME_WAIT 状態は MSL (Maximum Segment Lifetime = 最大セグメント生存時間) の 2倍の間続きます。これは TCP/IP の仕組み上、仕方のないことです。 OS によって bind 可能になるまでの時間が違うのは、MSL の値が違うからです。

つまり echo サーバは終了したつもりでも、実はポート 5000 番は TIME_WAIT になっていて、 完全には終了していないのです。 しかし SO_REUSEADDR オプションを設定すると、そのポートが TIME_WAIT 状態であっても、 bind することができます。…という難しいことは、とりあえずは覚えなくていいです。

なお、setsockopt の 2番目の引数 SOL_SOCKET ですが、 とにかく SOL_SOCKET と書けばよいのだ、と思って下さい (実は意味がよくわかりません)。

→ 関数説明: setsockopt

次に、ソケットにポート番号を割り当てます。
   17: bind(CLIENT_WAITING, pack_sockaddr_in($port, INADDR_ANY))
   18:      or die "bind に失敗しました。$!";
ソケットはあくまでも外界との出入口でしかなく、そのプロセス内だけで有効なものです。 bind することで初めてソケットとポート番号 (この場合は 5000) が結び付きます。

pack_sockaddr_in($port, INADDR_ANY) というのは、 「ポート番号は $port、IP アドレスは何でもよい」という意味です。 「なぜここで pack_sockaddr_in を使うのか、bind(CLIENT_WAITING, $port) でよいのではないか」 と思う方もいらっしゃるでしょうが、とりあえずそういうものだと思って下さい。

→ 関数説明: bind

   21: listen(CLIENT_WAITING, SOMAXCONN)
   22:      or die "listen: $!";
listen という関数名から、ここでクライアントからの接続を 待つような印象を受けますが、実は少し違います。 listen は クライアントからの接続を受け入れるように OS に指示する関数で、 実際にサーバプログラムとクライアントとの接続が確立されるのは次の accept です。 詳しくは次節のマルチスレッド版 echo サーバを作る際に説明します。 また、listen の3番目の引数には、同時に何個のクライアントを 受け入れるかを指定します。SOMAXCONN を指定すると、OS の限度いっぱいまで クライアント受け付けます。これも次節で説明します。
→ 関数説明: listen

クライアントがやってきた

   28:     $paddr = accept(CLIENT, CLIENT_WAITING);
accept が実行されるとクライアントからの接続を待ち続けます。 もし誰も接続してこなかったら、プログラムの実行はこの accept のところでストップしています(ブロックされる)。 この状態は、クライアントが接続してくるまで続きます。

いざクライアントが接続してきたら accept から処理が戻ります。 そして新しいソケットCLIENT が作られ、以後 クライアントとのやりとりは ソケット CLIENT を通して行うことになります。

ソケット CLIENT_WAITING は接続してきたクライアントとは無関係です。 あくまでも CLIENT_WAITING は「ポート監視用ソケット」なのです。 なぜ CLIENT と CLIENT_WAITING という2つのソケットが必要なのかは、 次節で詳しく説明します。
→ 関数説明: accept

accept が返す $paddr は、クライアントの情報です。 クライアント側のホスト名とポート番号をひとまとめにしたもので、 クライアントを作ったときにpack_sockaddr_in で作った構造体と同じ形式です。


   31:     ($client_port, $client_iaddr) = unpack_sockaddr_in($paddr);
   32:     $client_hostname = gethostbyaddr($client_iaddr, AF_INET);
   33:     $client_ip = inet_ntoa($client_iaddr);
   34: 
   35:     print "接続: $client_hostname ($client_ip) ポート $client_port\n";
この部分はデバッグ表示で、実際の動作には関係ありません。 $paddr は、ホスト名とポート番号をひとまとめにした構造体ですから、 unpack_sockaddr_in で「ポート番号と IP アドレスの構造体」に分解できます。
→ 関数説明: unpack_sockaddr_in
$client_iaddrgethostbyaddr でホスト名に変換できます。
→ 関数説明: gethostbyaddr
$client_iaddr は IP アドレスを表す構造体ですが、そのまま表示することは できません。そこで inet_ntoa で表示可能な IP アドレスの文字列に変換します。
→ 関数説明: inet_ntoa

   40:     while (<CLIENT>){
   41:         print "メッセージ: $_";
   42:                 # クライアントにメッセージを返す
   43:         print CLIENT $_;
   44:     }
クライアントからデータを受け取り、そのままクライアントに同じデータを 返しています (なぜなら echo サーバだからです)。
前へ << echo サーバを作ってみよう (1) echo サーバを作ってみよう (3) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。