素のPerlでWebサーバーにアクセスする
というわけで、今回もラジオねたではなく、前回のPerlのCGIからWebサービスにアクセスするの続きで「素のPerlでWebサーバーにアクセスするにはどうするのか?」です。コードはめんどいので正常系のみでいきます。もし実装するときはちゃんとエラー処理もいれてください。まずは、最初に注意事項として申し上げますが、CGIから他のサーバーにアクセスするようにしていると、そのCGIが大量に呼び出された場合でも、呼び出すサーバーに負荷がかからないようにすべきでしょう。さて、サンプルコードですが、connect()までは、世に存在するこの手のコードと一緒なので、詳細な説明は省きます。基本的にこういうものだと思ってもらって問題ないと思いますが…。$AF_INET = 2;$SOCK_STREAM = 1;$SockAddr = 'S n a4 x8';($name,$aliases,$proto) = getprotobyname('tcp');($name,$aliases,$type,$len,$localAddr) = gethostbyname('localhost');($name,$aliases,$type,$len,$serverAddr) = gethostbyname($host);$this = pack($SockAddr, $AF_INET, 0, $localAddr);$server = pack($SockAddr, $AF_INET, $port, $serverAddr);socket(S, $AF_INET, $SOCK_STREAM, $proto);bind(S, $this);connect(S, $server);ここで、$host、$portはアクセスするサーバーのホスト名(ドメイン表記でも可)とポート番号です。httpの場合は、普通$portは80になるでしょう。次はサーバーへリクエストを送ります。要するにHTTPプロトコルのGETメソッドを使ってコンテンツを持ってくる方法ということになります。binmode(S);select(S); $| = 1;$request = "GET $uri HTTP/1.1";$header = "Host: $host";print "$request\r\n";print "$header\r\n\r\n";select(STDOUT);binmode()はUNIX上では意味はないですが念のため。「$| = 1;」はこれを指定しておかないとバッファに溜まったままで、サーバーに送り出してくれません。最後の「select(STDOUT);」は、デフォルト出力先を元に戻しています。ここでは、$urlと$hostを指定しなければならないのですが、$hostは前のコードの$hostと同じで基本的にはOKです。$uriは、ここではサーバー内のパス(すなわちURLでhttp://hostname以降に書いてあるもの)を指定します(例えば、「/cgi-bin/test.cgi?param=value」)。ヘッダーの「Host:」はHTTP/1.1では必須なので省略不可で、あとお好みでヘッダーを追加してください。$header .= "\r\nUser-Agent: hogehoge";と、書いてみたりとか。これで、リクエストは送られましたので、あとは結果を読み込むわけですが、失敗しなければ、「ステータス」と「ヘッダー」と「リクエストの結果(コンテンツ)」が返ってきます。とりあえず、ヘッダーの解析までです。<S> =~ /^HTTP\/[.0-9]+ ([0-9]{3})/;if ($1 ne '200') {#どうやらエラー}while(<S>) {last if $_ eq "\r\n";$headers{$1} = $2 if /^(.+): (.+)$/;}というような、ちょっと強引なコードですが…。まずステータスコード「200」以外はエラーとみなしていますが、このあとヘッダー、場合によってはコンテンツも送られてきますので、ここはそのまま突っ走ります。たぶん、コードを保存しておいて、最後に処理するのが正しい方法かと思われます。ヘッダーの内容は後から使うので、連想配列の中に入れておきます。次にリクエストの結果を取得するのですが、サイズの指定され方によって基本的に3つの方法があります。まずはコード。$content = '';if($contentLength = $headers{'Content-Length'}) {read(S, $content, $contentLength);}elsif ($headers{'Transfer-Encoding'} =~ /^chunked/) {while(<S>){last unless /^([0-9a-fA-F]+)/ && $chunkLength = hex($1);read(S, $buf, $chunkLength);$content .= $buf;last unless <S> eq "\r\n";}}TCP/IP入門第2版Content-Lengthでサイズを指定Content-Lengthは10進数の数値で指定されてきますので、このサイズ分だけ読み込んだら終了するという処理になります。Transfer-Encodingがchunkedこの場合は、それ以後に「16進数の数値CRLF、数値のサイズ分のデーターCRLF」というのを繰り返しますが、サイズが「0」の場合にすべてのデーターが送られてきたということで終了となります。いずれの指定もなしコネクションが切れるまでがデーターとみなして送られてくるのですが、HTTP/1.1ではKeepAliveがあるためあり得ないはず。HTTP/1.0以前の処理用なので実装なし。このコードもエラーチェックなしなので、このままではどうかと思います。<S>やreadはエラーが起きるとundefを返すので、それをチェックしたほうがよいと思います。最後は、ソケットを閉じます。close(S);shutdown(S,2);できる限り、close(),shutdown()は、connect()成功後、いかなる場合においても実行しておくべきです。と、いうわけでこれで終わりです。ただし、1つ気になることがあって、それはタイムアウトをどうするのかということです。思いついた対処の方法としては…。本当に使える厳選CGIスクリプト集何もしないこれをCGIから呼ぶとすれば、WebサーバーがCGIを終了させたり、ソケットがタイムアウトするので、それに期待する。あまりよいとは思えないけど。setsockopt()でタイムアウトを設定一応、そういうオプション(SO_RCVTIMEO)がある。デフォルトよりも短く設定することで対応する。ただし、システム依存する可能性あり。監視用のプロセスを使って、ソケットを閉じるconnect()と同時に子プロセスをforkして、一定時間sleepさせる。もしsleepを抜けたら、ソケットをクローズ。そうすると、read()とかがエラーで読み込みのブロックから抜け出す(はず)。これもシステムに依存しそうな感じ。ちなみにうまくいったときは、子プロセスとkillする。監視用のプロセスを使って、シグナルを送るこれと同じようにconnect()と同時に子プロセスをforkして、一定時間sleepさせるが、抜けたら親プロセスにkill()でシグナル(INT)を送る。親プロセスはシグナルハンドラで適切な処理をして終了。ちょっと強引っぽいが。説明が詳しくない割には長いなぁ