lib/reel/connection.rb in reel-0.4.0.pre vs lib/reel/connection.rb in reel-0.4.0.pre2
- old
+ new
@@ -8,22 +8,24 @@
CONNECTION = 'Connection'.freeze
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
KEEP_ALIVE = 'Keep-Alive'.freeze
CLOSE = 'close'.freeze
- CHUNKED = 'chunked'.freeze
attr_reader :socket, :parser
# Attempt to read this much data
BUFFER_SIZE = 16384
+ attr_reader :buffer_size
- def initialize(socket)
+ def initialize(socket, buffer_size = nil)
@attached = true
@socket = socket
@keepalive = true
- @parser = Request::Parser.new
+ @parser = Request::Parser.new(socket, self)
+ @writer = Response::Writer.new(socket, self)
+ @buffer_size = buffer_size.nil? ? BUFFER_SIZE : buffer_size
reset_request
@response_state = :header
end
@@ -38,78 +40,59 @@
@attached = false
self
end
# Reset the current request state
- def reset_request(state = :header)
+ def reset_request(state = :ready)
@request_state = state
- @header_buffer = "" # Buffer headers in case of an upgrade request
+ @current_request = nil
@parser.reset
end
+ def readpartial(size = @buffer_size)
+ raise StateError, "can't read in the '#{@request_state}' request state" unless @request_state == :ready
+ @parser.readpartial(size)
+ end
+
+ def current_request
+ @current_request
+ end
+
# Read a request object from the connection
def request
- return if @request_state == :websocket
- req = Request.read(self)
+ raise StateError, "already processing a request" if current_request
- case req
- when Request
- @request_state = :body
- @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
- when WebSocket
- @request_state = @response_state = :websocket
- @socket = nil
- else raise "unexpected request type: #{req.class}"
- end
+ req = @parser.current_request
+ @request_state = :ready
+ @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
+ @current_request = req
req
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
# The client is disconnected
@request_state = :closed
@keepalive = false
nil
end
- # Read a chunk from the request
- def readpartial(size = BUFFER_SIZE)
- raise StateError, "can't read in the `#{@request_state}' state" unless @request_state == :body
+ # Enumerate the requests from this connection, since we might receive
+ # many if the client is using keep-alive
+ def each_request
+ while req = request
+ yield req
- chunk = @parser.chunk
- unless chunk || @parser.finished?
- @parser << @socket.readpartial(size)
- chunk = @parser.chunk
+ # Websockets upgrade the connection to the Websocket protocol
+ # Once we have finished processing a Websocket, we can't handle
+ # additional requests
+ break if req.websocket?
end
-
- chunk
end
- # read length bytes from request body
- def read(length = nil, buffer = nil)
- raise ArgumentError, "negative length #{length} given" if length && length < 0
-
- return '' if length == 0
-
- res = buffer.nil? ? '' : buffer.clear
-
- chunk_size = length.nil? ? BUFFER_SIZE : length
- begin
- while chunk_size > 0
- chunk = readpartial(chunk_size)
- break unless chunk
- res << chunk
- chunk_size = length - res.length unless length.nil?
- end
- rescue EOFError
- end
-
- return length && res.length == 0 ? nil : res
- end
-
# 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
+ raise StateError, "not in header state" if @response_state != :header
if headers_or_body.is_a? Hash
headers = headers_or_body
else
headers = {}
@@ -127,46 +110,66 @@
response = Response.new(response, headers, body)
when Response
else raise TypeError, "invalid response: #{response.inspect}"
end
- response.render(@socket)
+ @writer.handle_response(response)
# Enable streaming mode
- if response.headers[TRANSFER_ENCODING] == CHUNKED and response.body.nil?
+ if response.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_request(:header)
+ reset_request(:ready)
else
@socket.close unless @socket.closed?
reset_request(:closed)
end
end
# 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)
- @socket << chunk_header + Response::CRLF
- @socket << chunk + Response::CRLF
+ @writer.write(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}"
+ @writer.finish_response
@response_state = :header
end
# Close the connection
def close
+ raise StateError, "socket has been hijacked from this connection" unless @socket
+
@keepalive = false
@socket.close unless @socket.closed?
+ end
+
+ # Hijack the socket from the connection
+ def hijack_socket
+ # FIXME: this doesn't do a great job of ensuring we can hijack the socket
+ # in its current state. Improve the state detection.
+ if @request_state != :ready && @response_state != :header
+ raise StateError, "connection is not in a hijackable state"
+ end
+
+ @request_state = @response_state = :hijacked
+ socket = @socket
+ @socket = nil
+ socket
+ end
+
+ # Raw access to the underlying socket
+ def socket
+ raise StateError, "socket has already been hijacked" unless @socket
+ @socket
end
end
end