KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

HTTP

HTTPクライアントをpythonで作ろう

お初にお目にかかります。 ひよこエンジニアtakei-hです。ぴよぴよ。 さて、amo-k先輩からの課題にやっと手を付けました! さっそく本題です!

1.telnetを用いてHTTPリクエストを発行せよ!

telnetを用いて、僕らの味方「Yahoo!知恵袋」のトップページに対してHTTPリクエスト(GETメソッド)を発行してみましょう。 (リクエストメソッドにはGETのほかにPOSTなどがありますが、今回はGETのみを取り上げます)
telnet chiebukuro.yahoo.co.jp 80[エンター]
GET / HTTP/1.0[エンター][エンター]
上記のコマンドをコンソールで実行すると、webサーバからのレスポンスとして、以下に示されているヘッダーとメッセージボディ(Yahoo!知恵袋のHTML文書)が返ってきます。
HTTP/1.1 200 OK
Date: Wed, 07 Oct 2009 08:15:40 GMT
P3P: policyref="http://privacy.yahoo.co.jp/w3c/p3p.xml", CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
Set-Cookie: Ychie=KcPItpD.70E_gAXpes9zvcGQj_3HKkwW; path=/; domain=.chiebukuro.yahoo.co.jp
Cache-Control: private
Connection: close
Content-Type: text/html; charset=UTF-8

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="description" content="あなたの疑問や知りたいことを、他の参加者に質問できるYahoo!知恵袋。疑問に思っていることを質問したり、知っている事柄についての質問に回答することで、参加している方がお互いに知恵や知識を教えあい、分かち合えるQ&Aサイト">
<title>Yahoo!知恵袋</title>
・・・後は省略・・・
ヘッダーには、「HTTP/1.1 200 OK」つまりHTTP/1.1というプロトコルでステータスコード200(取得成功)でメッセージがOKとなっています。ヘッダーとメッセージボディの間に改行(\r\n)が入っています。 このように、HTTPセッションでは本来の目的であるHTML文書のやりとりの前に、様々な情報をお互いやり取りしているのですね。 そして、そのやり取りの手順(プロトコル)がHTTP(HyperText Transfer Protocol)なのですね!

2.任意の言語を用いてTCPソケットを利用したHTTPクライアントを作成せよ!

昔のhonda-h先輩はrubyで実装されていたので、私はpythonで実装してみました。GETで取得して、メッセージボディのみを表示してみましょう。
#!/usr/local/bin/python2.4

import socket

host = "chiebukuro.yahoo.co.jp"
port = 80
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
 sock.connect((host, port))
 sock.send("GET / HTTP/1.0\r\n\r\n")
 msg = ""
 while True:
  data = sock.recv(8192)
  if not data:
   break
  msg += data
 for m in msg.split("\r\n\r\n")[1:]:
  print m
 sock.close()
except socket.error, e:
 print "Error: %s" % e
TCPソケットを使うために
 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
でソケットオブジェクトをつくり、問題1と同じようにGETメソッドを送っています。
 sock.send("GET / HTTP/1.0\r\n\r\n")
そうすると、問題1と同じようなレスポンスが返ってきます。そのレスポンスを"\r\n\r\n"を区切りとしてヘッダーとメッセージボディを切り分けています。 HTTPサーバもクライアントも根本はTCPソケットの張り合い。つまりTCPソケットを制するものは、HTTPを制す!ですね! 勉強になりました! ついでに、pythonにはHTTPクライアントライブラリhttplibがあり、とっても簡単にhttpクライアントが作れるようになっています。
#!/usr/local/bin/python2.4

import httplib

h = httplib.HTTPConnection('chiebukuro.yahoo.co.jp')
h.request('GET', '/')
r = h.getresponse()
if r.status == httplib.OK:
 data = r.read()
 print data

IE × SSL × リソースDL × no-cache

ども、amo-kです。 今回は、先日RIA的なアプリを作っていて遭遇した 問題について書きます。 IE × SSL × リソースDL 上記が重なった際に生じる問題です。 どうなるかといいますと、 HTTPレスポンスヘッダのキャッシュコントロール系で 意図した動作をしないという問題です。 例えば、
Pragma: no-cache
というレスポンスヘッダを定義していると 逆にcacheファイルを参照しようとするようです。 今回は、HTTPサーバ側でレスポンスの全てを構築していた訳ではありませんでした。 サーバサイドはphpアプリケーションでレスポンスヘッダ/メッセージボディを生成するのですが phpはデフォルトでレスポンスヘッダを生成してくれます。 その際に
Pragma: no-cache
を入れてくれます。 これは嬉しいのですが、 上記IEのバグに対応するには、ヘッダを書き換える必要があります。 ということで以下のようにヘッダを上書きして対応しました。
header("Pragma: ");
一般的には、Webサイトの管理ツールなどで SSL通信による集計ファイルのDLサービス等で 同様の現象が再現し得るでしょう。 JavaScriptじゃないんだし、 phpソースコード中にIEの対応を書くなんてと思いましたが よくよく調べてみると、Microsoftサポートでも公開していたので まあいっかと思った次第です。 参考: Microsoft サポート オンライン

HTTPクライアントを書いてみた(Ruby Socketクラス)

amo-kさんにつづき、私(takda-at)もHTTPクライアントを実装してみました。 まずはRubyのコードです。Rubyでは、socketというネットワークプログラミング用のライブラリが標準で用意されています。その中でもTCPSocketなどのクラスを利用するとHTTPクライアントなども非常に簡単につくれるのですが、今回は勉強のため、あえて低レイヤーなところから書いています。 socketライブラリの中でも、Socketクラスは、ソケットをシステムコールレベルで操作するための機能を提供しています。メソッド名などもシステムコールと同じ名前が採用されているようです。 Rubyリファレンスマニュアルの説明にはそこまで詳細な解説が無いので、LinuxなどのManPageも合わせて見た方が参考になります。 また今回は勉強のために、socketライブラリのソースコードも少しのぞいてみました。socketライブラリはC言語で書かれたRubyの拡張ライブラリです。最新の安定板であるRuby1.8.7では、ruby-1.8.7-p72/ext/socket/socket.c にソースコードがあります。 参考 -Socket - Rubyリファレンスマニュアル- -Manpage of SOCKET -Manpage of GETHOSTBYNAME 難しかったのはSocket::connectを呼び出し、接続を行なうところです。このメソッドの引数には、バイナリデータを文字列の形でわたします。C 言語のconnect関数には、引数として、sockaddr構造体というものをわたすのですが、Socket::conncetメソッドの場合、Rubyの側からCの構造体を文字列の形でわたしてやる必要があります。 Rubyで、データをバイナリ文字列に変換するにはArrayクラスのpackメソッドを利用します。 Rubyでバイナリデータを扱うプログラムを書いたのははじめてだったので、非常に勉強になりました。 参考 -Manpage of CONNECT -Array::pack - Rubyリファレンスマニュアル -packテンプレート文字列 - Rubyリファレンスマニュアル なお、Socket::conncetメソッドは、socket.cの中では以下のように定義されています。
static VALUE
sock_connect(sock, addr)
    VALUE sock, addr;
{
    rb_io_t *fptr;
    int fd;

    StringValue(addr);
    addr = rb_str_new4(addr);
    GetOpenFile(sock, fptr);
    fd = fileno(fptr->f);
    if (ruby_connect(fd, (struct sockaddr*)RSTRING(addr)->ptr, RSTRING(addr)->len, 0) < 0) {
	rb_sys_fail("connect(2)");
    }

    return INT2FIX(0);
}
ruby_connect(fd, (struct sockaddr*)RSTRING(addr)->ptr, RSTRING(addr)->len, 0) という箇所で、 Ruby文字列(RSTRING)を、sockaddr*にキャストしているようです。 以下が作成したHTTPクライアントのコードです。URLをGETしてプリントするだけです。
#!/usr/bin/ruby -Ku
##httpc.rb: Rubyによる簡易HTTPクライアント


require 'socket'

class Httpc
    @@defaultport = 80
    def initialize
        @url
        @socket
        @host
        @port = @@defaultport
        @path
    end
    def open url,&b
        @url = url
        #url を host と pathに分解
        /http:\/\/([^\/]+)(.*)/ =~ url
        @host = $~[1]
        @path = $~[2]
        @path = "/index.html" if !@path || @path==""

        #ソケットを作成します。
        #Manpage of SOCKET
        #第1引数はプロトコロルファミリの指定です。AF_INETはIPv4を示す定数です。
        #第2引数は通信方式の指定です。
        #第3引数に0を指定することで、プロトコルファミリに見合ったプロトコルが選択されます。
        @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)


        #ホスト情報の取得
        #Manpage of GETHOSTBYNAME
        host = Socket.gethostbyname(@host)

        #connectの引数に必要な sockaddr_in構造体を作成しています。
        #Array::Packの引数については以下を参照してください。
        ##packテンプレート文字列 - Rubyリファレンスマニュアル
        sockaddr_in = ([Socket::AF_INET, @port]).pack("s1 n1")
        sockaddr_in += host[3] + [].pack("x8")

        #接続
        @socket.connect(sockaddr_in)
        
        
        reqh = "GET #{@path} HTTP/1.0\r\n"
        reqh += "HOST: #{@host}\r\n"
        reqh += "\r\n\r\n"
        #リクエストヘッダ送信
        @socket.print(reqh)


        if(!b) #ブロックがなければソケットを返す
            return @socket
        else  #ブロックがあればブロックにソケットを渡して実行
            yield @socket
            @socket.close
        end
    end
    def print(url)
        puts url
        self.open(url) do |s|
            state = 0
            until(s.eof?)
                buff = s.gets
                $stdout.print buff if(state==1)
                if(state==0)
                    state=1 if(buff=="\r\n")
                end
            end
        end
    end
end



def main
    if(!ARGV[0])
        puts "usage: ruby #{__FILE__} URI"
    else
        url = ARGV[0]
        h = Httpc.new
        h.print url
    end
end


main if(__FILE__==$0)

HTTPクライアントを書いてみた(Ruby open-uri, net/http, TCPSocket)

Ruby でHTTP通信をする方法はいくつかあります。 最も簡単なのは、open-uriを使う方法でしょう。 単純にあるURIに対してGETリクエストを送り、返されたHTMLを表示するだけなら、以下のように1行で済ませることもできます。
$ ruby -ropen-uri -e 'open(ARGV[0]){|f| puts f.read }' http://www.klab.jp/
しかし簡単な分、open-uriではPOSTができなかったり、制約もあります。そういうときは net/http を使うといいです。
require 'uri'
require 'net/http'

Net::HTTP.version_1_2

uri=URI(ARGV[0])

Net::HTTP.start(uri.host, uri.port){|http|
  puts http.get(uri.path).body
}
で、上と同じことが実現できます。 open-uri や net/http はライブラリでHTTPのレイヤーまで実装してくれているため、プログラマはそれを呼び出すだけでHTTPによる通信をすることができます。 しかし上のようなプログラムを書いただけでは、わかるのはあくまで「ライブラリの使い方」であって、今回の「HTTPやソケットについて勉強したい」という目的を果たしたとは言えません。 というわけで、もう少し低いレイヤーの部分から実装してみます。 ソケット通信をしたい場合、Rubyには 'socket' というそのままの名前のライブラリがあるので、それを使います。 その中にある TCPSocket というクラスを使い、その上でHTTPの処理を実装してみます。 以下のようになります。
require 'uri'
require 'socket'

uri = URI(ARGV[0])

socket = TCPSocket.new(uri.host, uri.port)
socket.puts "GET / HTTP/1.0\r\n"
socket.puts "Host: #{uri.host}\r\n"
socket.puts "\r\n"

puts socket.read.split("\r\n\r\n")[1..-1].join("\r\n\r\n")

socket.close
ソケット通信といっても非常に簡単です。
socket = TCPSocket.new(uri.host, uri.port)
で開いたソケットに対して、
socket.print "GET / HTTP/1.0\r\n"
socket.print "Host: #{uri.host}\r\n"
socket.print "\r\n"
でGETリクエストを送ります。 HTTPのヘッダの行末文字はCRLFであるとRFCで定められているので、各行末に "\r\n" をつけています。 サーバ側ではCRを無視してLFを行末として解釈することが推奨されているので、実際には "\r" をつけなくてもきちんとレスポンスが返ってくる場合がほとんどだと思いますが、予期しない不具合に悩まされる可能性もあるので、規定には従っておいたほうが無難です。 上の3行目で空行を入れ、ヘッダフィールドの終わりを示しています。GETリクエストはヘッダフィールドのみなのでこれで終わりですが、POSTリクエストの場合はこのあとに body-part として、対象に送るパラメータを記述します。 さて、上記のコードはTCPSocketを使ってソケット通信を開始し、その上でHTTPによる通信処理を実装しました。 今度はTCPSocketがやっているソケットの生成部分についても自分で実装・・・と、やろうとしたところで、すでに takada-at が実装してしまっていました。(^^; というわけで、次は takada-at がその実装について解説します。

HTTPクライアントを書いてみた

最初の投稿はSocketプログラミング! 最初のネタは、Socketプログラミングをしてみよう!というネタです。 通常、我々はライブラリやパッケージを使ってWebアプリケーションを実装します。 例えばアプリケーションから任意のHTTPリクエストするような処理を実装する際は、TCPクライアントの処理を意識せずにライブラリを呼び出すだけで実装できたりします。それでもいいっちゃいいのですが、あえて自前で実装してみようという試みです。 こうすることで、普段我々があまり意識していないTCP/IPの世界を意識するきっかけとなるのではと考えております。ということで、まずはSocket関数を用いてHTTPクライアントを書いてみることにします。 書いてみたきっかけ PHPやRuby等で何気に使っているHTTPクライアント。 PHPだとfopen()cURLでHTTPリクエストできたり、Rubyだとopen-uriのopen()等でHTTPリクエストができちゃいます。 ある日、自社Web API "FlaMixer" と連携する部分をPHPで書く機会があった。前述のfopen()やcURLのオプション指定だけでは連携が困難だったので、モジュールの使い方であれこれと悩むくらいなら自分で書いちゃえって事で書いてみました。これがきっかけでSocket関数を使ったコーディングを経験し、他の若手メンバにも経験してもらおう思いました。 ということで、言語はなんでもいいからHTTPクライアントを書いてみよう~ まずは言いだしっぺの若手amo-kのコード:
<?php
/*
 * TCPソケットを使ったHTTPクライアント
 *
 * 1.コマンドラインよりURL(絶対PATH)を取得
 * 2.HTTP GETリクエスト
 * 3.サーバからのメッセージボディを標準出力
 */
$url = $argv[1];
$port = 80;

// URLより、ホスト、ポート取得
if (!preg_match("/http:\/\/([^\/]+)(\/.*)/", $url, $match))
{
    echo "URLが不正です\n";
    exit;
}
$host = $match[1];
if (count($array = split(":", $host)) > 1)
{
    $host = $array[0];
    $port = $array[1];
}
$path   = $match[2];

$http_request_method = "GET";
$httpResponseHeader  = "";
$httpResponseBody    = "";

if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false)
{
    echo sprintf("ソケット生成失敗: %s\n", socket_strerror(socket_last_error()));
}
if (($res = socket_connect($socket, $host, $port)) === false)
{
    echo sprintf("ソケット接続失敗: %s%s\n", $res, socket_strerror(socket_last_error($socket)));
}

$httpRequest  = "$http_request_method $path HTTP/1.1\r\n";
$httpRequest .= "Host: $host\r\n";
$httpRequest .= "Connection: Close\r\n\r\n";

// HTTPリクエスト
socket_write($socket, $httpRequest, 1024);

// レスポンヘッダ取得
do
{
    $httpResponseHeader .= socket_read($socket, 8);
}
while (strpos($httpResponseHeader, "\r\n\r\n") === false);

// メッセージボディ取得
do
{
    $buf = socket_read($socket, 1024);
    $httpResponseBody .= $buf;
}
while ($buf);

// ソケットクローズ
socket_close($socket);

// メッセージボディ出力
echo $httpResponseBody;
?>
指摘いただきましたので、今度はもう少し丁寧に。 以下、該当部分のみの修正版です。 ついでにメッセージボディ取得部分も丁寧に書き直しました。 (別に乱暴に書いていたわけではないのですがw コメントありがとうございます! > とおりすがりさん
// レスポンヘッダ取得
do
{
    if (($buf = socket_read($socket, 1024, PHP_NORMAL_READ)) === false)
    {
        echo sprintf("レスポンスヘッダ取得失敗: %s\n", socket_strerror(socket_last_error($socket)));
        exit;
    }
    $httpResponseHeader .= $buf;
}
while (strpos($httpResponseHeader, "\r\n\r\n") === false);

// メッセージボディ取得
do
{
    if (($buf = socket_read($socket, 1024)) === false)
    {
        echo sprintf("メッセージボディ取得失敗: %s\n", socket_strerror(socket_last_error($socket)));
        exit;
    }
    $httpResponseBody .= $buf;
}
while ($buf);
 KLab若手エンジニアブログのフッター