色々な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は以下のように4つの要素から構成されます。
protocol//hostname:port pathname1つめの要素 protocol は通信の対話形式を指定するもので webでは主に http: (hyper text transfer protocol) を指定します。
filename/path_info?querry_string
与えられたURLの文字列から hostname, port, pathname を取り出す 作業は sscanf() 関数が適当です。scanf()のやや複雑な書式制御を 次のように利用します。
Scanf()系列での書式制御文字列 "%[^:]" は ':' の手前までの文字列を 読み込むことを意味します。"%hu"はu_shortを読み込むことを意味します。sscanf( url, "http://%[^:]:%hu%s", hostname, &port, pathname );
sscanf( url, "http://%[^/]%s", hostname, pathname );
ネットワークプログラムの内部では 通信相手の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バイトの整数が生で格納されています。 後でのこの取り扱いに注意が要ります。
次に、接続先の詳細を指定する書類のような役割をする sockaddr_in構造体型の変数を用意します。 この変数に書き込む内容は、接続対象が Internetのソケットであることと、 port番号と、そしてIPアドレスです。
hp->h_addr に対してキャスト演算子が必要です。// 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;
そしてInternet用のソケットを作成します。
作成したソケットをhttpdに接続します。// 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); }
HTTPDと接続したら、ダウンロードするファイルのpathnameや 種々の環境変数を送信します。 最も単純には例えば次のようなメッセージを送ります。
ここで改行はMS-DOSと同じ形式で"\r\n"で指定することに注意して下さい。 ファイルの要求の送信は次のようになります。"GET /~naoki/index.html\r\n"
// send a request to the httpd. len = sprintf( buf, "GET %s\r\n", pathname ); send( s, buf, len, 0 );
もう少し丁寧にファイルを要求するには例えば次のようなメッセージを送ります。
これらの情報は環境変数としてCGIやSSIに渡されます。ただしここで 指定した環境変数の名前は "HTTP_" を関した大文字で設定されます。"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"
さらに高度にFORMデータも送る場合には例えば次のような メッセージを送ります。
なお、ここで紹介する原始 browser は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"
さらにmutipartのFORMデータを送る場合には例えば次のような メッセージを送ります。
なお、ここで紹介する原始 browser はMultipart 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"
単に "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 をパイプ接続して起動して眺めます。
このファイルに先行して送られて来るヘッダーに このファイルの大きさが示されます。"GET /~naoki/image01.html HTTP/1.0\r\n" "Connection: Keep-Alive\r\n" "\r\n"
このサイズだけデータを読み込んでください。 読み込み終えても接続が続いていますので、そこで再びContent-Length: 2240
と送信して、次のファイルをダウンロードします。 最後のファイルの時には Connection: close として ダウンロード後に接続を切ってもらいます。"GET /~naoki/image02.html HTTP/1.0\r\n" "Connection: Keep-Alive\r\n" "\r\n"
なお、apacheの標準設定では1回の接続でダウンロードできる ファイルの最大数は 100個となっています。
このように自分のプログラムで WEBのデータを収集できるようになると、 次は HTML の HREF タグを解釈して、リンクを自動的にたどって次々に ファイルを集めていくロボット型サーチエンジンを作りたくなるでしょうが、 素人手でこの広いwebの世界へのアクセスを 猛烈に繰り返すことはネットワークにとって迷惑この上ありません。 ロボット型サーチエンジンは既にプロの手で十分便利なものがいくつも 用意されているので、自作することを考えてはいけません。