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

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

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若手エンジニアブログのフッター