ウェブブラウザを自作する


色々なCGIをC/C++言語で作ることを考えてきましたので、 今度は趣を異にして web browser をC/C++言語で作ることを考えてみましょう。
1990年代に登場した web browser は10年も経たないうちに驚くほどの進歩を 遂げました。始めのころの browser は単に指定URLのファイルを Internet経由でダウンロードするだけでした。その後の進歩は今更説明するまでも ないでしょう。
本章ではこの最も原始的な web browser の作り方を解説します。 単に指定URLのファイルをInternet経由でダウンロードして標準出力に表示するだけです。 HTMLタグの解釈は一切しませんし、画像を文書中に表示しません。 興味と自信のある人は、ここで解説する原始 web browser を改良して Lynx や mosaic 並の web browser を作成してみてはどうでしょうか? 10年前にそれを成し遂げていたらあなたもNetscape社の会長になれたでしょう。


● URLの解釈


■ URLの構成

URLは以下のように4つの要素から構成されます。

protocol//hostname:port pathname
1つめの要素 protocol は通信の対話形式を指定するもので webでは主に http: (hyper text transfer protocol) を指定します。
2つめの要素 hostname は通信相手のホストマシンの internet での名前を 指定します。
3つめの要素 port は通信相手のホストマシンのどのサービスに接続するかを 指定するものです。Httpdは標準では 80番port に待機しているので、 URLにportの指定を省略すると、80番portに接続します。
4つめの要素 pathname には要求するファイルの名前等を指定します。 この文字列はスラッシュ'/'から書き始めます。 pathnameはさらに次の3つの要素で構成されます。
filename/path_info?querry_string

■ URLの解釈

与えられたURLの文字列から hostname, port, pathname を取り出す 作業は sscanf() 関数が適当です。scanf()のやや複雑な書式制御を 次のように利用します。

sscanf( url, "http://%[^:]:%hu%s", hostname, &port, pathname );
Scanf()系列での書式制御文字列 "%[^:]" は ':' の手前までの文字列を 読み込むことを意味します。"%hu"はu_shortを読み込むことを意味します。
URLにportが指定されていない場合には、この sscanf() は 3 を返してこないので 次の sscanf() で再度解釈を試みます。
sscanf( url, "http://%[^/]%s", hostname, pathname );


● HTTPDとの接続


■ HostのIPアドレスの入手

ネットワークプログラムの内部では 通信相手のhostの指定は文字列の名前であるhostnameではなくて 4バイトの整数であるIPアドレスで指定します。 ネットワークシステムコール関数 gethostbyname() が hostnameからそのIPアドレスを調べる作業を一手に 行ってくれます。

  // get the IP address of the web server.
struct hostent* hp;
if( (hp = gethostbyname(hostname)) == NULL ){
  fprintf( stderr, "gethostbyname\n");
  exit(1);
}

ネットワークシステムコール関数 gethostbyname() はhostnameを 表す文字列へのポインタを引数として受けて、その名前のhostに 関して得られた情報を hostent構造体型の静的変数に格納して それへのアドレスを返します。gethostbyname()はlocalhostの/etc/hosts ファイルを参照したり、name serverと交信したりして目的のhostのIP アドレスを入手します。そのIPアドレスはこの例では hp->h_addr の メンバ変数に格納されています。ただし h_addr はchar型配列で、そのメモリ 領域に4バイトの整数が生で格納されています。 後でのこの取り扱いに注意が要ります。


■ HTTPD への Internet domain socket の接続指定書の作成

次に、接続先の詳細を指定する書類のような役割をする sockaddr_in構造体型の変数を用意します。 この変数に書き込む内容は、接続対象が Internetのソケットであることと、 port番号と、そしてIPアドレスです。

  // set the address of the internet domain socket.
struct sockaddr_in sadd;
sadd.sin_family = AF_INET;
sadd.sin_port   = htons(port);
sadd.sin_addr   = *(struct in_addr*) hp->h_addr;
hp->h_addr に対してキャスト演算子が必要です。


■ ソケットの作成と接続

そしてInternet用のソケットを作成します。

  // create a new socket.
int s = socket( AF_INET, SOCK_STREAM, 0 );
作成したソケットをhttpdに接続します。
  // connect the socket to the httpd.
if( connect( s, (sockaddr*)&sadd, sizeof(sockaddr_in) ) < 0 ){
  perror("connect");
  exit(1);
}

● HTTPDとの交信


■ ファイルの要求の送信

HTTPDと接続したら、ダウンロードするファイルのpathnameや 種々の環境変数を送信します。 最も単純には例えば次のようなメッセージを送ります。

"GET /~naoki/index.html\r\n"
ここで改行はMS-DOSと同じ形式で"\r\n"で指定することに注意して下さい。 ファイルの要求の送信は次のようになります。
  // send a request to the httpd.
len = sprintf( buf, "GET %s\r\n", pathname );
send( s, buf, len, 0 );

もう少し丁寧にファイルを要求するには例えば次のようなメッセージを送ります。

"GET /~naoki/index.html HTTP/1.0\r\n"
"Referer: http://www-cms.phys.s.u-tokyo.ac.jp/OURLAB/members.html\r\n"
"Connection: close\r\n"
"User-Agent: My original downloader\r\n"
"Host: localhost.my.domain\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*\r\n"
"\r\n"
これらの情報は環境変数としてCGIやSSIに渡されます。ただしここで 指定した環境変数の名前は "HTTP_" を関した大文字で設定されます。

さらに高度にFORMデータも送る場合には例えば次のような メッセージを送ります。

"POST /~naoki/cgitest.cgi HTTP/1.0\r\n"
"Referer: http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/index.html\r\n"
"Connection: Keep-Alive\r\n"
"User-Agent: My original downloader\r\n"
"Host: localhost.my.domain\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*\r\n"
"Content-type: application/x-www-form-urlencoded\r\n"
"Content-length: 49\r\n"
"\r\n"
"username=Naoki&password=secret&os=UNIX&language=C\r\n"
なお、ここで紹介する原始 browser はFORMに対応しません。

さらにmutipartのFORMデータを送る場合には例えば次のような メッセージを送ります。

"POST /~naoki/cgitest.cgi HTTP/1.0\r\n"
"Referer: http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/index.html\r\n"
"Connection: Keep-Alive\r\n"
"User-Agent: My original downloader\r\n"
"Host: localhost.my.domain\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*\r\n"
"Content-type: multipart/form-data; boundary=---------------------------836760821180171308267834847\r\n"
"Content-Length: 345\r\n"
"\r\n"
"-----------------------------836760821180171308267834847\r\n"
"Content-Disposition: form-data; name=\"author\"\r\n"
"\r\n"
"Naoki\r\n"
"-----------------------------836760821180171308267834847\r\n"
"Content-Disposition: form-data; name="fname"; filename=\"test.html\"\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"This is a test.\n"
"\r\n"
"-----------------------------836760821180171308267834847--\r\n"
なお、ここで紹介する原始 browser はMultipart FORMに対応しません。


■ ファイルの受信

単に "GET pathname\r\n" とした場合には直ちにファイルの内容が そのまま送り返されてきます。次のように単純に受信して標準出力に 書き出すことができます。

  // reveive contents and output them.
while( (len=recv( s, buf, sizeof(buf), 0 )) >0 ) write( 1, buf, len );

"GET pathname HTTP/1.0\r\n\r\n" とした場合には ファイルのheader情報がいくつか送り返された後、空行に続いてファイルの中身が 送り返されます。Header情報と中身を適切に分離するよう気を付けましょう。


■ 通信の切断

HTTPDはファイルを送信しおえると、その接続を切断します。 browser側も受信したら接続を切るようにします。

shutdown( s, 2 );

同じサイトの他のファイルを要求するなら、 ソケットの作成からやり直すだけです。


● 原始ブラウザのソース

以上をまとめると原始ブラウザ(名前は単に browser.cc )は次のようになります。 コマンド引数にURLを指定することにします。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main( int argc, char* argv[] )
{
  u_short port = 80;
  char hostname[256], pathname[256];

    // parse the URL.
  if( 2 != argc || (
      3 != sscanf( argv[1], "http://%[^:]:%hu%s", hostname, &port, pathname ) &&
      2 != sscanf( argv[1], "http://%[^/]%s", hostname, pathname ) ) ){
    fprintf( stderr, "Usage: browser http://hostname:port/pathname\n" );
    exit(1);
  }

    // get the IP address of the web server.
  struct hostent* hp;
  if( (hp = gethostbyname( hostname )) == NULL ){
    fprintf( stderr, "gethostbyname\n");
    exit(1);
  }

    // get the address of the internet domain socket to the HTTPD.
  struct sockaddr_in sadd;
  sadd.sin_family = AF_INET;
  sadd.sin_port   = htons(port);
  sadd.sin_addr   = *(struct in_addr*) hp->h_addr;

    // create a new socket.
  int s = socket( AF_INET, SOCK_STREAM, 0 );

    // connect the socket to the httpd.
  if( connect( s, (sockaddr*)&sadd, sizeof(sockaddr_in) ) < 0 ){
    perror("connect");
    exit(1);
  }

  size_t len;
  char   buf[4096];

    // send a request to the httpd.
  len = sprintf( buf, "GET %s\r\n", pathname );
  send( s, buf, len, 0 );

    // reveive contents and output them.
  while( (len=recv( s, buf, sizeof(buf), 0 ))>0 ) write( 1, buf, len );

  shutdown( s, 2 );

  return 0;
}


このプログラムは受信したファイルの内容をそのまま標準出力に出力します。 画像ファイル等はredirectでファイルに保存した後に viewer で眺めるか download と同時に viewer をパイプ接続して起動して眺めます。


● 原始ブラウザの応用

最近の apache では標準で KeepAlive の設定が On になっています。 これは一度の接続でいくつものファイルをダウンロードできるということです。 この機能を利用するには、HTTPDに接続した際に、以下のようにして Keep-Aliveと指定します。
"GET /~naoki/image01.html HTTP/1.0\r\n"
"Connection: Keep-Alive\r\n"
"\r\n"
このファイルに先行して送られて来るヘッダーに このファイルの大きさが示されます。
Content-Length: 2240
このサイズだけデータを読み込んでください。 読み込み終えても接続が続いていますので、そこで再び
"GET /~naoki/image02.html HTTP/1.0\r\n"
"Connection: Keep-Alive\r\n"
"\r\n"
と送信して、次のファイルをダウンロードします。 最後のファイルの時には Connection: close として ダウンロード後に接続を切ってもらいます。

なお、apacheの標準設定では1回の接続でダウンロードできる ファイルの最大数は 100個となっています。


● ネットワークに負担を掛けないないようにしましょう

このように自分のプログラムで WEBのデータを収集できるようになると、 次は HTML の HREF タグを解釈して、リンクを自動的にたどって次々に ファイルを集めていくロボット型サーチエンジンを作りたくなるでしょうが、 素人手でこの広いwebの世界へのアクセスを 猛烈に繰り返すことはネットワークにとって迷惑この上ありません。 ロボット型サーチエンジンは既にプロの手で十分便利なものがいくつも 用意されているので、自作することを考えてはいけません。


目次

Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp