インターネットに接続されているマシンは、IPアドレス によって識別されます。IPアドレスの表現形式には以下のものがあります。
形式 | 例 | 説明 |
---|---|---|
ホスト名形式 | "www.gihyo.co.jp" | 人間が覚え易い名前をつけた形式。 |
ドット形式 | "210.224.149.132" | 0~255 の数字を4つドット(.)で連結した形式。 |
バイナリ形式 | "\xd2\xe0\x95\x84" | ソケット関連の関数で用いられる形式。 |
ドット形式("210.224.149.132")からバイナリ形式("\xd2\xe0\x95\x84")への変換は次のようにします。
$dot_addr = "210.224.149.132"; $bin_addr = pack("C4", split(/\./, $dot_addr));
バイナリ形式("\xd2\xe0\x95\x84")からドット形式("210.224.149.132")への変換は次のようにします。
$bin_addr = "\xd2\xe0\x95\x84"; $dot_addr = join(".", unpack("C4", $bin_addr));
ホスト名形式("www.gihyo.co.jp")からバイナリ形式("\xd2\xe0\x95\x84")への変換は gethostbyname() を用います。gethostbyname() は DNSサーバーに問い合わせるなどして名前を解決します。
$host = "www.gihyo.co.jp"; $bin_addr = (gethostbyname($host))[4];
バイナリ形式("\xd2\xe0\x95\x84")からホスト名形式("www.gihyo.co.jp")は gethostbyaddr() を用います。DNSサーバーに問い合わせるなどして、アドレスから名前を逆引きします。
$bin_addr = "\xd2\xe0\x95\x84"; $host = gethostbyaddr($bin_addr, PF_INET);
ホスト名形式("www.gihyo.co.jp")からドット形式("210.224.149.132")への変換は、ホスト名形式 → バイナリ形式 → ドット形式の変換を組み合わせます。
$host = "www.gihyo.co.jp"; $bin_addr = (gethostbyname($host))[4]; $dot_addr = join(".", unpack("C4", $bin_addr));
ドット形式("210.224.149.132")からホスト名形式("www.gihyo.co.jp")への変換は、ドット形式 → バイナリ形式 → ホスト名形式の変換を組み合わせます。
$dot_addr = "210.224.149.132"; $bin_addr = pack("C4", split(/\./, $dot_addr)); $host = gethostbyaddr($bin_addr, PF_INET);
ソケット とは、インターネットアプリケーションを作成する際に使用される基本的なプログラミングインタフェース(APIの総称)、およびそのインタフェースで作成されるエンドポイント(ファイルハンドルのようなもの)を意味します。Webサーバやブラウザなど、ほとんどのインターネットアプリケーションはソケットを用いて作成されています。
インターネットアプリケーションでは、サービスを提供する側のアプリケーションやマシンを サーバ、サービスを要求する側のアプリケーションやマシンを クライアント と呼びます。
ソケットによる通信の基本パターンを以下に示します。TCP のタイプは、信頼性のある、コネクション型の、バイトストリーム通信を実現します。UDP のタイプは、信頼性はないけれども処理負荷の軽い、コネクションレスの、データグラム通信を実現します。それぞれの詳細は次節から説明します。
TCP通信 UDP通信 サーバ クライアント サーバ クライアント ======== =========== ========= ========= socket() socket() ↓ ↓ bind() bind() ↓ socket() ↓ socket() listen() ↓ ↓ ↓ ↓←←←←← connect() ↓ ↓ accept() ↓ ↓ ↓ ↓ ↓ ↓ ↓ recv() ←←←← send() recvfrom() ←←← sendto() send() →→→→ recv() sendto() →→→→ recvfrom() ↓ ↓ ↓ ↓ close() close() close() close()
ソケットによる、TCP タイプのサーバアプリケーションを作成する手順を紹介します。クライアントから送信されたメッセージを読み取り、小文字→大文字変換して返す簡単なサーバーアプリケーションです。サーバー作成後、クライアントを作成し、両者が完成したら動作させてみます。
socket() は ソケット S を作成します。ソケットはファイルハンドルのようなものと考えてください。引数に PF_INET, SOCK_STREAM, 0 を選択した場合は TCP が選択されます。UDP の場合は PF_INET, SOCK_DGRAM, 0 となります。PF_INET などの定数を使用するために Socket モジュールを読み込んでいます。
use Socket; socket(S, PF_INET, SOCK_STREAM, 0) || die "socket"; 続く↓
bind() はソケットに名前をつけます。電話機に電話番号を割り当てる行為に似ています。名前は、IPアドレスと ポート番号 から構成されます。IPアドレス 0.0.0.0 は、任意のIPアドレスを示す特別な値です。ポート番号にはとりあえず、5000~65535 までの、他のアプリケーションと重複しなさそうな数値を指定してください。
$ipaddr = pack("C4", split(/\./, "0.0.0.0")); $port = 8888; $name = pack("S n a4 x8", PF_INET, $port, $ipaddr); bind(S, $name) || die "bind"; 続く↓
listen() はソケットを接続待ち状態にします。5 は、同時に5個の接続要求を受けつけることができることを意味します。このカウンタはクライアントからの接続要求を受けつけると1減り、accept()が完了すると1増えます。
listen(S, 5) || die "listen"; 続く↓
accept() はクライアントからの接続要求を待ち、要求がくると通信のための新しいソケット(NS)を生成します。古いソケット(S)は引き続き別の接続要求待ちのために、新しいソケット(NS)は通信のために用いられます。
accept(NS, S) || die "accept"; 続く↓
ファイルハンドルと同じように、ソケットに対してデータを送受信するには <NS> や print NS を用います。例では、データがバッファリングされてしまうのを防ぐために、NS に対するバッファリングフラグ $| を 0 以外の値に設定しています。クライアントからの切断が行われるまで、同じ処理を繰り返しています。
$old = select(NS); $| = 1; select($old); while ($msg = <NS>) { # データを受信して print "$msg"; print NS "\U$msg\E"; # 大文字に変換して送り返す } 続く↓
close() はソケットをクローズします。接続待ちに用いた S と、通信に用いた NS の両方をクローズしています。
close(NS); close(S);
次は、クライアント側のアプリケーションを作成します。
クライアント側アプリケーションでは、サーバ側アプリケーションに接続し、メッセージを送信し、大文字に変換されたメッセージを受信し、それを表示します。
ソケットの生成は、サーバ側アプリケーションと同じです。
use Socket; socket(S, PF_INET, SOCK_STREAM, 0) || die "socket"; 続く↓
connect() はクライアントからサーバに接続要求を行います。電話番号を指定して電話をかける行為に似ています。電話番号の代わりにサーバーの IPアドレスとポート番号を指定します。localhost は自ホストを示す特別な名前です。他のマシンに接続するには、www.xxx.zzz のようなサーバー名を指定してください。8888 は、サーバー側で指定したポート番号です。
$addr = (gethostbyname("localhost"))[4]; $port = 8888; $name = pack("S n a4 x8", PF_INET, $port, $addr); connect(S, $name) || die "connect"; 続く↓
print でメッセージを送信し、<S> でその結果を受信し、print で結果を表示します。$| により、ソケット S に対するバッファリング機能をオフにしています。
$old = select(S); $| = 1; select($old); print S "Hello!!\n"; $msg = <S>; print $msg; print S "This is Client.\n"; $msg = <S>; print $msg; print S "Bye.\n"; $msg = <S>; print $msg; 続く↓
通信が完了したら、用済みのソケット S をクローズします。
close(S);
前節で作成したソケットアプリケーションを動かしてみます。コマンドプロンプトを2つ起動し、片方でサーバ側を、もう片方でクライアント側を動かしてください。サーバー側の実行イメージは次のようになります。
% perl server.pl Hello. This is Client. Bye. %
クライアント側の実行イメージは次のようになります。
% perl client.pl HELLO. THIS IS CLIENT. BYE. %
ここで紹介したのは文字列を大文字に変換するという簡単なサーバーアプリケーションでしたが、クライアントからファイル名を受け取り、そのファイルを読み込んで返却すると、ウェブサーバーの基礎の出来上がりです。その他の様々なインターネットアプリケーションも、このソケットによる通信をベースにして構築されています。
select() を用いることで、複数のクライアントからの要求に対してサービスを行うサーバーを作成することができます。標準の出力先を変更する select() と同じ名前ですが、機能はまったく異なります。
select() は、複数のソケットハンドルやファイルハンドルを監視し、そのうち 1つ以上のハンドルがアクティブになるまで待ちます。ソケットハンドル S1 と S2 の受信待ちを監視するには次のようにします。$rin には監視するソケットハンドルのディスクリプタをビット列で指定します。
$rin = ""; vec($rin, fileno(S1), 1) = 1; vec($rin, fileno(S2), 1) = 1; $nf = select($rout = $rin, undef, undef, undef);
$nf には見付かったハンドルの個数が返されます。どのハンドルが読み込み可能になったかどうかは $rout を参照します。
if (vec($rout, fileno(S1), 1)) { print "S1が読み込み可能\n"; } if (vec($rout, fileno(S2), 1)) { print "S2が読み込み可能\n"; }
select() の完全な形式は次のようになります。
$tout = 30.0; ($nf, $tleft) = select($rout = $rin, $wout = $win, $eout = $ein, $tout);
$tout にタイムアウト時間(秒数)を指定すると、$tleft で残り秒数が返されます。$rin は読み込み可能状態、接続受付可能状態、切断状態を監視します。$win は書き込み可能状態を監視します。$ein は例外処理状態を監視します。
複数のクライアントからの接続を受け付け、クライアントが送信したメッセージを大文字に変換して送り返すサービスの例を示します。ソケットディスクリプタリストの他に、ソケットハンドルを管理するためのソケット番号リストを用いていますので、注意してください。詳細はスクリプト中のコメントを参照してください。
use Socket; # ソケットを作成してLISTEN状態にしておく $ipaddr = pack("C4", split(/\./, "0.0.0.0")); $port = 8888; $name = pack("S n a4 x8", PF_INET, $port, $ipaddr); socket(S, PF_INET, SOCK_STREAM, 0) || die "socket"; bind(S, $name) || die "bind"; listen(S, 5) || die "listen"; # 受信用のソケットディスクリプタリストを初期化する $rin = ""; vec($rin, fileno(S), 1) = 1; # ソケット番号リストを初期化する @socklist = (); # 無限ループ(Ctrl-Cで終了) for (;;) { # どれかのソケットがアクティブになるのを待つ $nfound = select($rout = $rin, undef, undef, undef); # 接続待ち受けソケット(S)であれば接続受け付け処理へ if (vec($rout, fileno(S), 1)) { DoAccept(); } # データ用ソケット(NS?)であれば受信処理へ for ($n = 1; $n <= 64; $n++) { if ($socklist[$n]) { $sock = "NS$n"; if (vec($rout, fileno($sock), 1)) { DoReceive($n); } } } }
# 接続要求受付時の処理 sub DoAccept { # 1~64の中で未使用の番号を探す for ($n = 1; $n <= 64; $n++) { if (!$socklist[$n]) { last; } } # 未使用のものが無ければ接続してすぐに切断 if ($n > 64) { accept(NS, S); close(NS); return; } # ソケットを作成する "NSソケット番号" $sock = "NS$n"; accept($sock, S) || die "accept"; print "accept: $sock\n"; # バッファリングしないモードにしておく # select($rout....) とは別物 $old = select($sock); $| = 1; select($old); # ソケットディスクリプタリストに追加 vec($rin, fileno($sock), 1) = 1; # ソケット番号リストに追加 $socklist[$n] = 1; } # データ受信時の処理 sub DoReceive { local($n) = @_; local($sock) = "NS$n"; if ($msg = <$sock>) { # メッセージを受信したら print "recv: $sock $msg"; # 大文字に変換して返す print $sock "\U$msg\E"; print "send: $sock \U$msg\E"; } else { # 相手がクローズしたら # ソケットディスクリプタリストから削除 vec($rin, fileno($sock), 1) = 0; # ソケット番号リストから削除 $socklist[$n] = 0; # こちらもクローズする close($sock); print "close: $sock\n"; } }