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)