TCPSocket練習

webrickプロキシに乗せるための試作httpクライアント。
前にNet::BufferedIO(内部用と書いてあるのを勝手に使っている)を流用していた部分がどうにも使い勝手がよろしくないので自作することにした。
TCPSocketを使えば楽ちん。
一挙に全データをreadしないという要件(動画などの巨大データを逐次で送る)のため、httpヘッダを捌く部分がこんなんになってしまった。

#!/usr/bin/ruby
# coding: utf-8

require 'uri'
require 'socket'

module HTTP

  # HTTPヘッダを行毎に読み込む補助クラス
# よろしくないので書き直し
#  class SocketReader
#    def initialize(tcp_socket)
#      @tcp_socket=tcp_socket
#      @lines=[]
#      @line_break=false
#    end
#
#    def getlines
#      buf=@tcp_socket.read(128)
#      lines=buf.split("\r\n")
#      if not @lines.empty? and @line_break then
#        @lines[@lines.size-1]+=lines.shift
#      end
#      @lines+=lines
#      @line_break=(not buf.end_with?("\r\n"))
#    end
#
#    def gets
#      case @lines.length
#      when 0
#        getlines
#      when 1
#        while @lines.length==1 and @line_break do
#          getlines
#        end
#      end
#      return @lines.shift.rstrip
#    end
#
#    def flush
#      #return @lines.join("\n") バイナリデータが壊れる時がある
#      return @lines.join("\r\n")
#    end
#  end

  class SocketReader
    def initialize(tcp_socket)
      @tcp_socket=tcp_socket

      @buf=''
      while true
        buf=@tcp_socket.read(128)
        if not buf then
          break
        end
        @buf << buf
        index=@buf.index("\r\n\r\n")
        if index then
          @lines=@buf.slice!(0, index+4).split("\r\n")
          break
        end
      end
    end

    def gets
      @lines.shift
    end

    def flush
      return @buf
    end
  end


  class Client
    attr_accessor :status, :response, :body, :version, :message
    def get(uri)
      # open socket
      ############################################################
      begin
        socket=TCPSocket.open(uri.host, uri.port)
      rescue SocketError=>e
        p e
        return false
      end
      # send http request
      ############################################################
      socket << "GET #{uri.path} HTTP/1.0\r\n"
      socket << "\r\n"

      # read http response
      ############################################################
      # response
      reader=SocketReader.new(socket)

      # header
      m=reader.gets.match(%r|HTTP/(\d\.\d) (\d+) (.*)|)
      @version=m[1]
      @status=m[2].to_i
      @message=m[3]
      @response={}
      while true
        line=reader.gets
     if not line then
          break
        end
        if line=="" then
          break
        end
        key, value=line.split(":", 2)
        @response[key]=value.strip
      end

      # body
      @body=reader.flush
      while true
        buf=socket.read(1024)
        if not buf then
          break
        end
        @body << buf
      end

      return true
    end
  end
end

def main(url)
  c = HTTP::Client.new
  if not c.get url then
    puts "fail to get #{url}"
    return
  end
  p c.status, c.message
  p c.response
  p c.body.bytesize
end

if __FILE__==$0 then
  if(!ARGV[0])
    puts "usage: ruby #{__FILE__} URI"
    exit
  end
  main URI.parse(ARGV[0])
end