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

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

2008年10月

HTTPクライアントを書いてみた(C++)

takada-atです。 Rubyでソケットをいじっていたら、同じものをC/C++でも書いてみたくなりました。 そこで、C++でもHTTPクライアントに挑戦してみました。C/C++はよくわからないので、変なコードになっていると思いますが、遠慮なくつっこみをいただけるとうれしいです。 (そもそもコードが長すぎる気がします。。)

#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

using namespace std;

class Httpc{
    int BUFF_LEN;
public:
    int port;
    int soc;
    Httpc(){
        BUFF_LEN = 256;
        port = 80;
    }
    vector<string> parse_url(string url){
        int p = url.find("://");
        string path;
        url = url.substr(p+3);
        p = url.find("/");
        if(p==string::npos){
            path = "/";
        }else{
            path = url.substr(p+1);
            url = url.substr(0,p);
        }
        //cout << "host:" << url << " path:" << path << endl;
        vector<string> r;
        r.push_back(url); r.push_back(path);
        return r;
    }
    void openc(string url){
        vector<string> urls = parse_url(url);
        hostent *host;
        sockaddr_in addrin;

        soc = socket(AF_INET, SOCK_STREAM, 0);
        string reqh;
        host = gethostbyname( (urls[0]).c_str() );
        memset(&addrin, 0 ,sizeof(addrin));
        addrin.sin_family = AF_INET;
        addrin.sin_addr.s_addr = *(in_addr_t*)host->h_addr;
        addrin.sin_port = htons(port);
        connect(soc, (sockaddr*)&addrin, sizeof(addrin));
        reqh = "GET "+ urls[1] +" HTTP/1.0\r\n";
        reqh += "HOST: "+ urls[0]+ "\r\n";
        reqh += "\r\n\r\n";
        //cout << reqh << endl;
        send(soc, reqh.c_str(), reqh.length(), 0);
    }
    void closec(){
        close(soc);
    }
    void print(string url){
        this->openc(url);
        char buff[1024];
        char hh[1024];
        char s[1024];
        int r = 1;
        int state = 0;
        int i=0, p;
        if(r<0){
            state = 3;
        }
        while(r>0){
            r = read(soc, buff, sizeof(buff));
            if(state==0){
                for(i=0;i<sizeof(buff);++i){
                    if(state==0){
                        if(buff[i]=='\r' && buff[i+1]=='\n'){
                            ++i;
                            state = 1;
                        }
                    }else if(state==1){
                        if(buff[i]=='\r' && buff[i+1]=='\n'){
                            state = 2;
                            p = i;
                            break;
                        }else{
                            state = 0;
                        }
                    }
                }
                if(state==2)
                    cout << buff+p+2 << endl;
            }else if(state==2){
                write(1,buff,r);
            }

        }
    }
};
int main(int argc, char *argv[]){
    Httpc c;
    c.print(argv[1]);
}

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(株)の若手エンジニアがブログを始めることになりました。 日々の業務における技術的トピックや、何気なく使っている便利なライブラリの内部で行われている処理を覗いた時のハナシ。これ等をブログというカタチで面白いものを発信できるのでは、という発想からこのブログをはじめることとなりました。 まだまだ未熟な若手エンジニアですが、技術・知識に対するアグレッシヴなアプローチや新しいものを試すチャレンジ等見どころ満載なブログです! コメントなど大歓迎です、ぜひチェックしてください!!
 KLab若手エンジニアブログのフッター