lib/reel/connection.rb in reel-0.0.2 vs lib/reel/connection.rb in reel-0.1.0

- old
+ new

@@ -1,46 +1,52 @@ module Reel # A connection to the HTTP server class Connection - class StateError < RuntimeError; end # wrong state for a given request + class StateError < RuntimeError; end # wrong state for a given operation attr_reader :request # Attempt to read this much data BUFFER_SIZE = 4096 def initialize(socket) @socket = socket @keepalive = true @parser = Request::Parser.new - reset + reset_request @response_state = :header @body_remaining = nil end # Is the connection still active? def alive?; @keepalive; end + # Reset the current request state + def reset_request + @request_state = :header + @request = nil + @parser.reset + end + + # Read a request object from the connection def read_request raise StateError, "can't read header" unless @request_state == :header begin - until @parser.headers - @parser << @socket.readpartial(BUFFER_SIZE) - end + @parser << @socket.readpartial(BUFFER_SIZE) until @parser.headers rescue IOError, Errno::ECONNRESET, Errno::EPIPE @keepalive = false @socket.close unless @socket.closed? return end @request_state = :body headers = {} - @parser.headers.each do |header, value| - headers[Http.canonicalize_header(header)] = value + @parser.headers.each do |field, value| + headers[Http.canonicalize_header(field)] = value end if headers['Connection'] @keepalive = false if headers['Connection'] == 'close' elsif @parser.http_version == "1.0" @@ -49,10 +55,11 @@ @body_remaining = Integer(headers['Content-Length']) if headers['Content-Length'] @request = Request.new(@parser.http_method, @parser.url, @parser.http_version, headers, self) end + # Read a chunk from the request def readpartial(size = BUFFER_SIZE) if @body_remaining and @body_remaining > 0 chunk = @parser.chunk unless chunk @parser << @socket.readpartial(size) @@ -65,40 +72,72 @@ chunk end end - def respond(response, body = nil) + # Send a response back to the client + # Response can be a symbol indicating the status code or a Reel::Response + def respond(response, headers_or_body = {}, body = nil) + raise StateError "not in header state" if @response_state != :header + + if headers_or_body.is_a? Hash + headers = headers_or_body + else + headers = {} + body = headers_or_body + end + if @keepalive - headers = {'Connection' => 'Keep-Alive'} + headers['Connection'] = 'Keep-Alive' else - headers = {'Connection' => 'close'} + headers['Connection'] = 'close' end case response when Symbol response = Response.new(response, headers, body) when Response else raise TypeError, "invalid response: #{response.inspect}" end response.render(@socket) + + # Enable streaming mode + if response.headers['Transfer-Encoding'] == "chunked" and response.body.nil? + @response_state = :chunked_body + end rescue IOError, Errno::ECONNRESET, Errno::EPIPE # The client disconnected early @keepalive = false ensure if @keepalive - reset + reset_request @request_state = :header else @socket.close unless @socket.closed? @request_state = :closed end end - def reset - @request_state = :header - @request = nil - @parser.reset + # Write body chunks directly to the connection + def write(chunk) + raise StateError, "not in chunked body mode" unless @response_state == :chunked_body + chunk_header = chunk.bytesize.to_s(16) + Response::CRLF + @socket << chunk_header + @socket << chunk + end + alias_method :<<, :write + + # Finish the response and reset the response state to header + def finish_response + raise StateError, "not in body state" if @response_state != :chunked_body + @socket << "0" << Response::CRLF * 2 + @response_state = :header + end + + # Close the connection + def close + @keepalive = false + @socket.close end end end