前へ << echo サーバを作ってみよう (1) | echo サーバを作ってみよう (3) >> 次へ |
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-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 から 接続してきたわけです。
というわけで、ポート 1024 番以上なら何番でもいいのですが、 キリのいいところで 5000 番にしましょう。
7: $port = 5000;
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 を通して行うことになります。
→ 関数説明: 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_iaddr は gethostbyaddr でホスト名に変換できます。
→ 関数説明: 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 までお願いします。