広告
広告
https://www.7key.jp/nw/study3.html#top「Perl でネットワークのお勉強」第三弾としまして、HTTPを題材に勉強を進めましょう。 HTTP はご存知の通り、Webサーバとファイルをやり取りするためのプロトコルです (詳細はHTTP とはを参照下さい)。 プロトコルの話だけで言いましたら、実はHTTP の方が SMTP より幾分か話が簡単です。 「Perl でネットワークのお勉強 第二段」、 「SMTP とは」の辺りを読んで頂ければ分かると思いますが、 SMTP サーバにお願い事をするときは、何回もリクエストとレスポンスをやり取りしなければなりませんでした。 しかし、Web サーバから HTML ファイルをレスポンスとして返してもらうためのリクエストは1回だけです。 つまり、Web サーバに対して、「ファイルを下さい」とリクエストを送信すれば、Web サーバは(基本的に)それだけでレスポンスを返してくれるのです。 余談ではありますが、HTML ファイルであろうが画像ファイルであろうが、そのレスポンスの中にデータとして含まれています。
今回はこのことを利用して、Web サイトのソースを取り出す関数を考えようと思います。
「Web サーバからのレスポンス(文字列)」= &getHtml('URL');
というサブルーチンが理想ですね。 これは意外と応用範囲が広くアイデア次第で様々な使い方ができると思います。 ヘッダ情報のみレスポンスが返ってくるようにしていても、リンク集のリンク切れを調べたり HTML ファイルの最終更新日時を調べたりできますし、 HTML ソースまで返ってくるようにすれば、CGI 等で動的に作成される Web ページの最新情報を取得したり Reffer サイトのタイトルだけを取得したりすることもできます。 まぁ、使い道の方は各自にお任せするとして、とりあえず形にしていきましょう。 ある程度汎用的に使えるよう、関数は以下の形で作っていきます。
「レスポンス」= &getHtml('URL', 'アクセス元', '送信データ', 'タイムアウト時間(秒)', 'プロクシ');
第一引数の「URL」は、当然受け取りたいファイルのアドレスです。 第二引数の「アクセス元」は参照元URLとも言い、どこからそのファイルを参照したかを表します。 リンク元URL とでも言った方が分かり易いですかね。 通常は使わないと思いますので空白でよいでしょう。 第三引数の「送信データ」は、CGIファイル等にデータをPOSTする際に使用するつもりです。 こちらも通常は空白でよいでしょう。 第四引数の「タイムアウト時間」は、サーバからの応答を受け取る際に、 何秒までなら待ちますか、という値を入れるつもりです。 通常は「5」や「10」で十分でしょうね。 最後に、第五引数の「プロクシ」にはプロクシサーバ情報を指定させようかと思っています。 企業内LAN など、プロクシを使用しなければならない環境であれば必須となってくるでしょう。 'プロクシサーバアドレス:ポート番号'の形で指定し、使用しなければ空白といった形を考えています。 以上の引数を駆使して、Web サーバからレスポンスをもらうプログラムを作っていきます。
まずは例のごとく大枠からですが、理解しやすくするためにまずどのようなHTML リクエストを作るかを見てみましょう。 当ページのURL である「https://www.7key.jp/program/perl/study3.html」をリクエストする場合、 以下のようなデータをWeb サーバに送信します(ブラウザはもっと多くのデータを送信していますが、今回は必要最低限ってことで)。
GET /program/perl/study3.html HTTP/1.1 Referer: http://www.hogehoge.jp/ Host: www.7key.jp Connection: keep-alive
各項目が何を表しているか分からない方は、HTTP とはを参考にして下さい。 プロクシを使用する場合はとりあえず横に置いときまして、基本的なプログラムの流れは、
となります。
sub getHtml {
    use Socket;
    my ($strURL, $strReffer, $strSend, $lngTime, $proxy) = @_;
    my ($port, $rtnHtml, $socket_add);
    my ($dmy1, $dmy2, $strHost, $strFile) = split(/\//, $strURL, 4);
    $strFile = '/'. $strFile;
    eval{
    };
    alarm(0);
    if($@){return 0;}else{return $rtnHtml;}
}
今回も当然ソケットを使用しますので、「use Socket;」を忘れず記入して下さい。
そして、「@_」で引数を変数に代入し、後々に使うであろう変数を定義しておきます。
ここまでは基本中の基本なので特に問題も無いでしょう。
次に、「split 関数」を使用してURLから
ドメイン名と取得したいファイル名を抜き取ります。
もし、URLが「https://www.7key.jp/program/perl/study3.html」であれば、
「http:」と「」と「www.7key.jp」と「program/perl/study3.html」に分割され、
「$sthHost = www.7key.jp」「$strFile = program/perl/study3.html」と代入されます。
$strFile = '/'. $strFile;
ついでにファイル名の先頭に「/」をつけておけば、リクエストラインと メッセージヘッダは作れることになります。
次に、構造体を作るためにポート番号を調べておきます。
ポート番号を指定してある場合にはそれをそのまま使用し、指定の無い場合には第二弾(SMTP編)の時と同様に、
「getservbyname 関数」を使用してポート番号を調べます。
($strHost, $port) = split(/\:/, $strHost);
if ($port eq ''){$port = getservbyname('http','tcp');}
ただし、プロクシを使用する場合には、今まで用意してきました「ホスト名」「ポート番号」「ファイル名」は全て通用しません。 ホスト名、ポート番号は当然プロクシのホスト名、ポート番号に、ファイル名はURLに変更しなければなりませんのでその記述を加えておきます。
if ($proxy ne ''){
    ($strHost, $port) = split(/\:/, $proxy, 2);
    $strFile = $strURL;
}
ここまでくれば後は力任せでプログラムを記述するだけですが、今回は新しいテクニックを紹介しましょう。
とは言いましても実は第一弾(PortScan編)でもこっそり使っていたのですが…。
引数の中に「タイムアウト時間」というものを用意していましたよね。
もし、「eval{」「};」内の処理が「タイムアウト時間」以上かかるようであれば、evalを抜け出してエラーを返すようにします。
こうすることにより、タイムアウトの際にも安全にプログラムを終了させることができるのです。
実際の使用方法は下記の通りです。
eval{
    local $SIG{ALRM} = sub{die "time out $!"};
    alarm($lngTime);
    "処理"
    alarm(0);
};
alarm(0);
「alarm 関数」を使用してまずタイマーをセットします。
この関数によって、引数の秒数が経過するとALRM シグナルを発するようシステムに要求をしています。
もし、シグナルが発せられたならば、「sub{die "time out $!"};」が実行され、
「$@」にエラー内容を代入して直ちにevalを抜け出します。
ここで一つ注意点ですが、
eval を抜け出す直前にalarm(0);として、タイマーをキャンセルすることを忘れないで下さい。
die で eval を抜け出すことも考えて念のため eval の直後でもタイマーをキャンセルしておきます。
そして例のごとく構造体を作成し、ソケットを用意し、Web サーバに接続を試みます。 ソケットのバッファリングも忘れず行っておきましょう。
$socket_add = pack_sockaddr_in($port, inet_aton($strHost));
socket (SCK, PF_INET, SOCK_STREAM, 0) || die("ソケットの生成失敗 $!");
connect(SCK, $socket_add) || die("接続失敗 $!");
select(SCK); $| = 1; select(STDOUT);
ここまでくれば、後はソケットを使用してWeb サーバとデータのやり取りを行うだけです。 その内容も含めて全文を紹介します。
sub getHtml {
    use Socket;
    my ($strURL, $strReffer, $strSend, $lngTime, $proxy) = @_;
    my ($port, $rtnHtml, $socket_add);
    my ($dmy1, $dmy2, $strHost, $strFile) = split(/\//, $strURL, 4);
    $strFile = '/'. $strFile;
    ($strHost, $port) = split(/\:/, $strHost);
    if ($port eq ''){$port = getservbyname('http','tcp');}
    if ($proxy ne ''){
        ($strHost, $port) = split(/\:/, $proxy, 2);
        $strFile = $strURL;
    }
    eval{
        local $SIG{ALRM} = sub{die "time out $!"};
        alarm($lngTime);
        $socket_add = pack_sockaddr_in($port, inet_aton($strHost));
        socket (SCK, PF_INET, SOCK_STREAM, 0) || die("ソケットの生成失敗 $!");
        connect(SCK, $socket_add) || die("接続失敗 $!");
        select(SCK); $| = 1; select(STDOUT);
        if($strSend eq ''){print SCK "GET $strFile HTTP/1.1\n";}
        else              {print SCK "POST $strFile HTTP/1.1\n";}
        print SCK "Referer: $strReffer\n";
        print SCK "Host: $strHost\n";
        print SCK "Connection: keep-alive\n";
        if ($strSend ne ''){
            my ($lngSend) = length($strSend);
            print SCK "Content-Length: $lngSend\n";
            print SCK "\n";
            print SCK "$strSend\n";
        }
        print SCK "\n";
        while (<SCK>) {
            $rtnHtml .= "$_";
            /<\/html>/ && last;
        }
        close(SCK);
        jcode::convert( \$rtnHtml , 'sjis' );
        alarm(0);
    };
    alarm(0);
    if($@){return 0;}else{return $rtnHtml;}
}
広告