HTTP クライアントを作ってみよう(2)

前へ << HTTP クライアントを作ってみよう(1) HTTP クライアントを作ってみよう(3) >> 次へ

HTTP クライアント

ではお待ちかねの HTTP クライアントのソースを見てみましょう。 Socket モジュールを使っているので、Perl5 専用です。

http-client.pl

    1: #!/usr/local/bin/perl -w
    2: 
    3: # $Id: http-client.pl,v 1.3 2003/03/23 11:28:03 68user Exp $
    4:                 
    5: use strict;
    6: 
    7: use Socket;     # Socket モジュールを使う
    8: 
    9:                 # 接続先ホスト名
   10: my $host = 'www.cs.gunma-u.ac.jp';
   11: 
   12:                 # HTTP プロトコルを使う
   13: my $port = getservbyname('http', 'tcp');
   14: 
   15:                 # ホスト名を、IP アドレスの構造体に変換
   16: my $iaddr = inet_aton($host)
   17:         or die "$host は存在しないホストです。\n";
   18: 
   19:                 # ポート番号と IP アドレスを構造体に変換
   20: my $sock_addr = pack_sockaddr_in($port, $iaddr);
   21: 
   22:                 # ソケット生成
   23: socket(SOCKET, PF_INET, SOCK_STREAM, 0)
   24:         or die "ソケットを生成できません。\n";
   25: 
   26:                 # 指定のホストの指定のポートに接続
   27: connect(SOCKET, $sock_addr)
   28:         or die "$host のポート $portに接続できません。\n";
   29: 
   30:                 # ファイルハンドル SOCKET をバッファリングしない
   31: select(SOCKET); $|=1; select(STDOUT);
   32: 
   33:                 # WWW サーバに HTTP リクエストを送る
   34: print SOCKET "GET /index.html HTTP/1.0\r\n";
   35: print SOCKET "\r\n";
   36: 
   37:                 # ヘッダ部分を受け取る
   38: while (<SOCKET>){
   39:                 # 改行のみの行ならループを抜ける
   40:     m/^\r\n$/ and last;
   41: }
   42: 
   43:                 # ボディ部分を受け取り、表示
   44: while (<SOCKET>){
   45:     print $_;
   46: }
思ったより短いでしょう? では少しずつ解説しましょう。

HTTP クライアントの解説

   10: my $host = 'www.cs.gunma-u.ac.jp';
まず、接続先のホスト名を決めます。
   13: my $port = getservbyname('http', 'tcp');
この行はおまじないです。HTTP プロトコルを使うから 'http' なんです。
この説明では納得できない人は読んで下さい。 getservbyname の serv というのは service の略です。HTTP のポート番号は 80 であり、 SMTP は 25 であると決まっています。ですから $port = 80 と書いても動作するのですが、 いきなり $port = 80 と書いても、ソースを読んでいる人にとっては、 どうして突然 80 という数字が出てきたのか わかりません。 そこで `HTTP' を 80 に変換してくれる getservbyname という関数を使うことで 「HTTP というプロトコルを使う」 ということをわかりやすくしているのです。 プロトコル名とポート番号の対応表は、UNIX なら /etc/services に、 Windows なら C:\windows\services に、Windows NT なら C:\WinNT\system32\drivers\etc\services に置いてあります。

では `TCP' とは何かというと、TCP/IP というプロトコルを使うことを宣言しているのです。 「プロトコルは HTTP と言ったり、TCP と言ったり、いったいどっちなんだ?」 と思った人へ。TCP と HTTP は層 (レイヤ) が違います。TCP はトランスポート層、 HTTP はアプリケーション層です。HTTP というのはただの文字列のやりとりですが、 その下には TCP という より低レベルな仕組みが働いています。 TCP を使うと、データが相手先に届いたかどうか、エラーが発生していないかどうか などを、OS が影でチェックしてくれます。

→ 関数説明: getservbyname

   16: my $iaddr = inet_aton($host)
   17:         or die "$host は存在しないホストです。\n";
$host には 'www.cs.gunma-u.ac.jp' という文字列が入っています。他のホストに接続するためには ホスト名でなく、IP アドレス (を4バイトの構造体にしたもの) を使わないといけません。 そこで、inet_aton を使って、ホスト名を IP アドレスに変換します。 inet_aton を呼び出すと、OS が勝手に DNS サーバに問い合わせて IP アドレスに変換してくれます。

もし変換に失敗した場合はそのホストは存在しない (存在するかもしれないけど、DNS に問い合わせても IP アドレスに変換できない) ということなので終了します。

→ 関数説明: inet_aton

   20: my $sock_addr = pack_sockaddr_in($port, $iaddr);
IP アドレスとポート番号をひとまとめにした構造体を生成します。 これは次の connect のために必要になります。
→ 関数説明: pack_sockaddr_in

   23: socket(SOCKET, PF_INET, SOCK_STREAM, 0)
   24:         or die "ソケットを生成できません。\n";
ソケットを生成します。 ソケットというのは、プロセスと外界を繋ぐための出入口のようなものです。 UNIX では、ネットワークごしのデータのやりとりには、必ずソケットを使います。 PF_INET、SOCK_STREAM というのは定数で、Socket モジュールが 値をセットしてくれます。
PF_INET とはインターネットに接続する、 という意味で、SOCK_STREAM は TCP 接続する、ということを 表します。わざわざこういうことを書くということは、 他の値を与えることもできます。PF_INET の代わりに PF_LOCAL とすれば UNIX ドメインソケットを生成します。 SOCK_STREAM ではなく SOCK_DGRAM を指定すると、 UDP 接続になります。 しかし、両方ともこの web では扱いませんので忘れて下さい。

ソケットを使うと、ファイルを読み書きするのと同じ書き方で、 他のホストとデータのやりとりができます。 普通のファイルハンドルをオープンする場合、open(IN,"...") などとしますが、ソケットの場合は socket(SOCKET, ...) でオープンします。接続した後は、ファイルハンドルと同じように

print SOCKET "....";
で、データの送信ができます。
→ 関数説明: socket

   26:                 # 指定のホストの指定のポートに接続
   27: connect(SOCKET, $sock_addr)
   28:         or die "$host のポート $portに接続できません。\n";
いよいよ接続 (connect) です。これが正常に行えたらあとはサーバとの データのやりとりに入ります。もし connect に失敗したら www.cs.gunma-u.ac.jp でポート80を見張っている WWW サーバが存在しないのです。 サーバがいないとクライアントがどれだけがんばっても通信はできません。

あるいは、www.cs.gunma-u.ac.jp というホストがない、例えばマシンの電源が 入っていなかったり、ネットワークに接続されていないのかもしれません。

→ 関数説明: connect

   31: select(SOCKET); $|=1; select(STDOUT);
ソケットに対してバッファリングしないようにします。これもおまじないだと思って下さい。
perl に限らず、UNIX にはバッファリングというシステムがあります。 例えば print 文を実行しても、一定サイズの文字がたまるか、改行コードが出力されるまで 実際には出力されず、OS がバッファに蓄積しています。 こうすることで効率のよい入出力ができるのです。ところがサーバに 届けるデータをOS がバッファリングしてしまうと、クライアントはデータを送ったつもりなのに サーバにはいつまでたっても届かないということになってしまいます。 それを避けるためソケットに対するバッファリング機能を OFF にしているのです。

ほとんどがおまじない

以上でサーバへの接続が完了しました。

ほとんどの説明で「これはおまじない」と書きましたよね。 もちろん意味を理解しているにこしたことはありませんが、 TCP/IP プログラミングをする限り、 この部分はほとんど共通なのです。POP3 クライアントでも SMTP クライアントでも 違うのは「接続するホスト名」と「ポート番号」だけです。つまり

$port = getservbyname(プロトコル名,'tcp');
$iaddr = inet_aton(接続するホスト名)
この部分を書き換えるだけで、その他は全て同じです。

サーバへの送信

いよいよ HTTP プロトコルの送信です。
   34: print SOCKET "GET /index.html HTTP/1.0\r\n";
   35: print SOCKET "\r\n";
ソケットに対してデータを送ります。これは telnet で行ったものと同じですね。 改行が \n (LF) でなく \r\n (CRLF) なのは、「HTTP プロトコルの改行は \r\n である」 と RFC で決められているからです。

サーバからの受信

次にデータ受信です。
   37:                 # ヘッダ部分を受け取る
   38: while (<SOCKET>){
   39:                 # 改行のみの行ならループを抜ける
   40:     m/^\r\n$/ and last;
   41: }
   42: 
   43:                 # ボディ部分を受け取り、表示
   44: while (<SOCKET>){
   45:     print $_;
   46: }
サーバから送られてきたデータを、ソケットを通じて1行ずつ読み込みます。

telnet で試したように、サーバはヘッダ・空行・ボディを順に送ってきます。 とりあえず表示したいのはボディなので、空行(改行のみの行)がくるまでは、 読み込んだ内容は表示しません。

注意してほしいのは、ソケットというのはデータ送信とデータ受信を 同じファイルハンドルで扱うということです。 送信は print OUT "..."; 、受信は while (<IN>){ などということはなくて、送受信を SOCKET という1つのファイルハンドルで 行います。

このクライアントは後始末を何もしていません。 これは HTTP プロトコルの流れが

  • クライアントがサーバにリクエストを送信
  • サーバからクライアントにヘッダとボディを送信
  • 送信が終了したらサーバがコネクションを切断
と決まっているからです。サーバが勝手にコネクションを 切断してくれるので、クライアント側は後始末をしなくてもいいのです。
前へ << HTTP クライアントを作ってみよう(1) HTTP クライアントを作ってみよう(3) >> 次へ

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