HTTP の並行アクセス

前へ << モジュールを使ってみよう (3) HTTP proxy サーバを作ってみよう >> 次へ

並行アクセス

IO::Socket と IO::Select の習作として、複数のページを並行して 読み込む HTTP クライアントを作成しましょう。
  • http://www.goo.ne.jp/
  • http://www.yahoo.co.jp/
  • http://www.asahi.com/
この3つのページを読み込むには、
use IO::Socket;
@servers = qw(www.goo.ne.jp www.yahoo.co.jp www.asahi.com);

foreach $server (@servers){
  $socket = IO::Socket::INET->new(PeerAddr => $server,
                                  PeerPort => 80,
                                  Proto    => 'tcp',
                                  );
  print $socket "GET /index.html\r\n";
  print $socket "Host: $server:80\r\n";
  $socket->flush();

  print <$socket>;
}
とすればよいです。しかし、これでは http://www.goo.ne.jp/ の読み込みが 終らないと http://www.yahoo.co.jp/ に進みません。http://www.yahoo.co.jp/ が 終らないと http://www.asahi.com/ に進みません。これを並行して読み込むことで、 実行時間の短縮を計りましょう。まずはスクリプトです。

http-client-parallel.pl

    1: #!/usr/local/bin/perl
    2: 
    3: # $Id: http-client-parallel.pl,v 1.4 2005/09/03 21:23:21 68user Exp $
    4: 
    5: use IO::Socket;
    6: use IO::Select;
    7: 
    8: # 以下のサーバの web トップページを取得する
    9: my @servers = qw(www.goo.ne.jp www.yahoo.co.jp www.asahi.com);
   10: 
   11: my $port = 80;
   12: 
   13: my $selecter = IO::Select->new;             # select の前準備
   14: 
   15: foreach my $server (@servers){
   16:     
   17:     my $sock = IO::Socket::INET->new("$server:$port");  # 各サーバのソケットを生成
   18: 
   19:     $selecter->add($sock);                              # select の対象ソケットに追加
   20:     $sock2host{$sock} = "$server:$port";                # メッセージ表示用ハッシュ
   21: 
   22:     print $sock "GET / HTTP/1.0\r\n";                   # データの送信
   23:     print $sock "Host: $server:$port\r\n";
   24:     print $sock "\r\n";
   25: 
   26:     $sock->flush();                                     # バッファをフラッシュ
   27: }
   28: 
   29: # 読み込むべきデータが残っているソケット数。初期値はサーバ数と同じ
   30: my $last_sock = $#servers+1;
   31: 
   32: # 読み込みが完了していないソケットが残っていたら
   33: while ( $last_sock > 0 ){
   34:     my ($readable_socks) = IO::Select->select($selecter, undef, undef, undef);
   35: 
   36:     # 読み込み可能なソケットがあれば、以下の foreach ループが実行される
   37:     foreach my $sock (@$readable_socks){
   38:         my $len = sysread($sock, $buf, 4096);   # ソケットから 4096 バイト読み込む
   39: 
   40:         if ( $len > 0 ){                # 1バイト以上読み込めた
   41: 
   42:             # 読み込んだ内容を整形して表示
   43: 
   44:             $buf =~ s/^(.{20}).*/$1/s;  # 先頭 20バイト以降を捨てる
   45:             $buf =~ s/^/      /mg;      # 先頭にスペースを挿入
   46:             $buf =~ s/[\r\n]//g;        # 改行コードを省く
   47:             print "read ${len}bytes from $sock2host{$sock} $buf....\n";
   48:         
   49:         } else {                        # 1バイトも読み込めなかった
   50: 
   51:             print "fin $sock2host{$sock}\n";  # そのソケットからの読み込みは終了
   52:             $selecter->remove($sock);         # select の対象から外す
   53:             $sock->close();                   # ソケットをクローズ
   54:             $last_sock--;                     # 残りソケット数を減らす
   55:         }
   56:     }
   57: }

少しずつ解説していきましょう。
    5: use IO::Socket;
    6: use IO::Select;
まず、IO::Socket モジュールと IO::Select モジュールを使うことを宣言します。
    9: my @servers = qw(www.goo.ne.jp www.yahoo.co.jp www.asahi.com);
この3サイトのトップページを読み込みます。@servers = qw(A B C) というのは @servers = ('A', 'B', 'C') と同じ意味です。いちいちクォートしたり カンマを書く必要がないので お勧めです。
   13: my $selecter = IO::Select->new;             # select の前準備
IO::Select が返すオブジェクトを $selecter に代入します。
   15: foreach my $server (@servers){
   16:     
   17:     my $sock = IO::Socket::INET->new("$server:$port");  # 各サーバのソケットを生成
   18: 
   19:     $selecter->add($sock);                              # select の対象ソケットに追加
   20:     $sock2host{$sock} = "$server:$port";                # メッセージ表示用ハッシュ
   21: 
   22:     print $sock "GET / HTTP/1.0\r\n";                   # データの送信
   23:     print $sock "Host: $server:$port\r\n";
   24:     print $sock "\r\n";
   25: 
   26:     $sock->flush();                                     # バッファをフラッシュ
   27: }
配列 @servers を順に $server に代入し、それぞれに対して
  • IO::Socket::INET->new でソケットを生成し、$sock に代入
  • ソケット $sock を $selecter に追加し、select の対象ソケットとする
  • print $sock で HTTP リクエストを送信
  • $sock->flush() で、バッファをフラッシュ
という処理を行います。$sock2host{$sock} というのは、 この後の結果表示のときに使うハッシュですから、実際の動作とは 関係ありません。
   30: my $last_sock = $#servers+1;
$last_sock には読み込みが完了していないソケットの数を代入します。 ここでは配列 @servers の要素数、この場合は 3 が代入されるはずです。
   33: while ( $last_sock > 0 ){
上で設定した $last_sock が 0 を越えている場合、 つまり読み込むべきソケットが残っている間、while ループを実行します。
   34:     my ($readable_socks) = IO::Select->select($selecter, undef, undef, undef);
$selecter に登録したソケットの中で、読み込み可能となっている ソケットがあれば $readable_socks に返されます。その時点で全てのソケットが 読み込み可能となっていなければ (読み込むべきデータが届いていなければ)、 いずれかのソケットが読み込み可能になるまで IO::Select->select() から 帰ってきません。
   37:     foreach my $sock (@$readable_socks){
   38:         my $len = sysread($sock, $buf, 4096);   # ソケットから 4096 バイト読み込む
$readable_socks は「配列へのリファレンス」が代入されますので、 @$readable_socks は「配列」です。各要素は「読み込み可能なソケット」です。
よくわからない人は、print "$readable_socks $sock\n" などとして、実体が 何なのか調べてみてください。
次に sysread でソケットから 4096 バイト読み込み、読み込んだデータを $buf に代入します。 また、実際に読み込んだデータの長さを $len に代入します。

普通、データが届いていないソケットに対して sysread しようとするとブロック (データが届くまで動作が止まってしまうこと) されてしまいます。 しかし IO::Select で読み込み可能なソケットのみを調べ、それに対して sysread を行っていますので、 ブロックは起こりません。 よって、$readable_socks に入っているソケットは、

  • 1バイト以上読み込み可能 ($len>0)
  • コネクションがクローズされた ($len==0)
のいずれかである、ということになります。データを読み込めたなら
   40:         if ( $len > 0 ){                # 1バイト以上読み込めた
   41: 
   42:             # 読み込んだ内容を整形して表示
   43: 
   44:             $buf =~ s/^(.{20}).*/$1/s;  # 先頭 20バイト以降を捨てる
   45:             $buf =~ s/^/      /mg;      # 先頭にスペースを挿入
   46:             $buf =~ s/[\r\n]//g;        # 改行コードを省く
   47:             print "read ${len}bytes from $sock2host{$sock} $buf....\n";
読み込んだデータの先頭部分 20バイトを切り出し、表示します。 20バイトの部分がちょうど日本語の1バイト目と2バイト目にあたる場合は文字化けしますが、 サンプルということでそのままにしています。
   49:         } else {                        # 1バイトも読み込めなかった
   50: 
   51:             print "fin $sock2host{$sock}\n";  # そのソケットからの読み込みは終了
   52:             $selecter->remove($sock);         # select の対象から外す
   53:             $sock->close();                   # ソケットをクローズ
   54:             $last_sock--;                     # 残りソケット数を減らす
   55:         }
$len==0 ならば、コネクションがクローズされた、 つまり読み込みが完了したということなので、$selecter->remove で select の対象ソケットから外し、ソケットをクローズします。 そして残りソケット数である $last_sock を1つ減らします。

実行結果

このスクリプトを実行すると
read 2920bytes from www.goo.ne.jp:80       HTTP/1.1 200 OK      Ser....
read 1460bytes from www.yahoo.co.jp:80       HTTP/1.0 200 OK      Cont....
read 588bytes from www.yahoo.co.jp:80       kusai20001017/?http:....
read 1460bytes from www.goo.ne.jp:80       ="http://community.g....
read 137bytes from www.asahi.com:80       HTTP/1.1 200 OK      Ser....
read 1460bytes from www.goo.ne.jp:80       ex.html">仕事</a><br....
read 1460bytes from www.yahoo.co.jp:80       >      <br>            </small>            </....
read 1460bytes from www.goo.ne.jp:80                                    あなたの企業HPに....
read 1460bytes from www.asahi.com:80       <!-- home format #2 ....
read 888bytes from www.asahi.com:80       82" height="64" usem....
read 499bytes from www.goo.ne.jp:80       ml" target="_blank">....
fin www.goo.ne.jp:80
read 1460bytes from www.yahoo.co.jp:80       .co.jp/shopping20001....
read 1460bytes from www.yahoo.co.jp:80       ks/literature_and_no....
read 1460bytes from www.yahoo.co.jp:80       ews/Newspapers/">新...
read 1460bytes from www.asahi.com:80             <!--L....
read 1460bytes from www.yahoo.co.jp:80       f="/homeb/?http://ww....
read 463bytes from www.yahoo.co.jp:80       enter>            <BR>            <cente....
fin www.yahoo.co.jp:80
read 1130bytes from www.asahi.com:80       "/tech/feature/20001....
read 1460bytes from www.asahi.com:80       ref="http://mytown.a....
read 1181bytes from www.asahi.com:80       ubscribe2.html">w...
read 301bytes from www.asahi.com:80       <a href="/event.ng/T....
read 1460bytes from www.asahi.com:80              <!-- L^C....
read 1460bytes from www.asahi.com:80       ews/international290....
read 892bytes from www.asahi.com:80       T(00:57)<br>....
read 1460bytes from www.asahi.com:80       hr noshade>      </td>      </....
read 1460bytes from www.asahi.com:80       N`G....
read 287bytes from www.asahi.com:80       area shape="rect" co....
fin www.asahi.com:80
となります (途中一部を省略しています)。 並行して3ホストからデータを読み込めていることがわかりますね。
   37:     foreach my $sock (@$readable_socks){
sysread では 4096 バイト分読もうとしていますが、最大で 2920 バイト、 最小で 301 バイト、大半は 1460 バイトとなっています。

sysread (システムコール read(2)) を使うと、 その時点で届いている分のデータを取得できます。1バイトかもしれませんし、 1KB かもしれません。1460 バイト読み込むことが多いのは、Ethernet における 最大フレーム長が 1500 バイトだからです。IP データグラムの最大サイズは 65535 バイトなのですが、それを転送する下位プロトコルでのサイズ制限のため 1500 バイトの IP データグラムに分割されるわけです。このうち 40 バイトは IP ヘッダのため、実データは残りの 1460 バイトになります。

もし sysread の代わりに read (標準 I/O ライブラリ fread(3)) を使うと、 指定した 4096 バイトを読み込むまで返ってきません。ライブラリ内部で 指定サイズ分のデータの到着を待ってしまうわけです。これでは 本当の並行動作にはなりませんので注意して下さい。

課題

このサンプルでは、データ受信時は並行動作していますが、 以下の部分は並行動作しません。
   13: my $selecter = IO::Select->new;             # select の前準備
   14: 
   15: foreach my $server (@servers){
   16:     
   17:     my $sock = IO::Socket::INET->new("$server:$port");  # 各サーバのソケットを生成
   18: 
   19:     $selecter->add($sock);                              # select の対象ソケットに追加
   20:     $sock2host{$sock} = "$server:$port";                # メッセージ表示用ハッシュ
   21: 
   22:     print $sock "GET / HTTP/1.0\r\n";                   # データの送信
   23:     print $sock "Host: $server:$port\r\n";
   24:     print $sock "\r\n";
   25: 
IO::Socket::INET->new では、ホスト名を IP アドレスに 変換するため、内部で gethostbyname が行われます。 そのとき DNS サーバに問い合わせを行いますが、IP アドレスが得られるか、 一定時間が経過しタイムアウトになるまで、その先の処理は行われません。

また、TCP コネクションを接続する部分は、内部で 3 way handshake が行われますが、 これも並行動作はできません。

ただし、ソケットに print で HTTP リクエストを送信する部分は、 相手側にパケットが届く前に次の処理が行われます。 これは、TCP がバッファリングしているからで、このバッファが 溢れたときは print 文で動作が止まり、相手の受信待ちになることがあります。 しかし、これくらいの小さなリクエストの場合は大丈夫でしょう。

この辺は勉強中。
前へ << モジュールを使ってみよう (3) HTTP proxy サーバを作ってみよう >> 次へ

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