reel 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of reel might be problematic. Click here for more details.
- data/CHANGES.md +4 -0
- data/README.md +4 -3
- data/lib/reel.rb +1 -1
- data/lib/reel/connection.rb +54 -15
- data/lib/reel/{request/parser.rb → request_parser.rb} +0 -1
- data/lib/reel/response.rb +36 -6
- data/lib/reel/version.rb +1 -1
- data/spec/reel/connection_spec.rb +36 -5
- data/spec/reel/response_spec.rb +34 -0
- metadata +16 -13
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -15,16 +15,17 @@ applications and provides traditional multithreaded blocking I/O support too.
|
|
15
15
|
Connections to Reel can be either non-blocking and handled entirely within
|
16
16
|
the Reel::Server thread, or the same connections can be dispatched to worker
|
17
17
|
threads where they will perform ordinary blocking IO. Reel provides no
|
18
|
-
built-in thread pool, however you can build one yourself using Celluloid
|
18
|
+
built-in thread pool, however you can build one yourself using Celluloid.pool,
|
19
19
|
or because Celluloid already pools threads to begin with, you can simply use
|
20
20
|
an actor per connection.
|
21
21
|
|
22
22
|
This gives you the best of both worlds: non-blocking I/O for when you're
|
23
23
|
primarily I/O bound, and threads for where you're compute bound.
|
24
24
|
|
25
|
-
### Is
|
25
|
+
### Is it any good?
|
26
26
|
|
27
|
-
Yes
|
27
|
+
[Yes](http://news.ycombinator.com/item?id=3067434),
|
28
|
+
but it has room for improvement. A "hello world" web server benchmark,
|
28
29
|
run on a 2GHz i7 (OS X 10.7.3). All servers used in a single-threaded mode.
|
29
30
|
|
30
31
|
Reel performance on various Ruby VMs:
|
data/lib/reel.rb
CHANGED
data/lib/reel/connection.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Reel
|
2
2
|
# A connection to the HTTP server
|
3
3
|
class Connection
|
4
|
-
class StateError < RuntimeError; end # wrong state for a given
|
4
|
+
class StateError < RuntimeError; end # wrong state for a given operation
|
5
5
|
|
6
6
|
attr_reader :request
|
7
7
|
|
@@ -12,7 +12,7 @@ module Reel
|
|
12
12
|
@socket = socket
|
13
13
|
@keepalive = true
|
14
14
|
@parser = Request::Parser.new
|
15
|
-
|
15
|
+
reset_request
|
16
16
|
|
17
17
|
@response_state = :header
|
18
18
|
@body_remaining = nil
|
@@ -21,13 +21,19 @@ module Reel
|
|
21
21
|
# Is the connection still active?
|
22
22
|
def alive?; @keepalive; end
|
23
23
|
|
24
|
+
# Reset the current request state
|
25
|
+
def reset_request
|
26
|
+
@request_state = :header
|
27
|
+
@request = nil
|
28
|
+
@parser.reset
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read a request object from the connection
|
24
32
|
def read_request
|
25
33
|
raise StateError, "can't read header" unless @request_state == :header
|
26
34
|
|
27
35
|
begin
|
28
|
-
until @parser.headers
|
29
|
-
@parser << @socket.readpartial(BUFFER_SIZE)
|
30
|
-
end
|
36
|
+
@parser << @socket.readpartial(BUFFER_SIZE) until @parser.headers
|
31
37
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
32
38
|
@keepalive = false
|
33
39
|
@socket.close unless @socket.closed?
|
@@ -37,8 +43,8 @@ module Reel
|
|
37
43
|
@request_state = :body
|
38
44
|
|
39
45
|
headers = {}
|
40
|
-
@parser.headers.each do |
|
41
|
-
headers[Http.canonicalize_header(
|
46
|
+
@parser.headers.each do |field, value|
|
47
|
+
headers[Http.canonicalize_header(field)] = value
|
42
48
|
end
|
43
49
|
|
44
50
|
if headers['Connection']
|
@@ -51,6 +57,7 @@ module Reel
|
|
51
57
|
@request = Request.new(@parser.http_method, @parser.url, @parser.http_version, headers, self)
|
52
58
|
end
|
53
59
|
|
60
|
+
# Read a chunk from the request
|
54
61
|
def readpartial(size = BUFFER_SIZE)
|
55
62
|
if @body_remaining and @body_remaining > 0
|
56
63
|
chunk = @parser.chunk
|
@@ -67,11 +74,22 @@ module Reel
|
|
67
74
|
end
|
68
75
|
end
|
69
76
|
|
70
|
-
|
77
|
+
# Send a response back to the client
|
78
|
+
# Response can be a symbol indicating the status code or a Reel::Response
|
79
|
+
def respond(response, headers_or_body = {}, body = nil)
|
80
|
+
raise StateError "not in header state" if @response_state != :header
|
81
|
+
|
82
|
+
if headers_or_body.is_a? Hash
|
83
|
+
headers = headers_or_body
|
84
|
+
else
|
85
|
+
headers = {}
|
86
|
+
body = headers_or_body
|
87
|
+
end
|
88
|
+
|
71
89
|
if @keepalive
|
72
|
-
headers
|
90
|
+
headers['Connection'] = 'Keep-Alive'
|
73
91
|
else
|
74
|
-
headers
|
92
|
+
headers['Connection'] = 'close'
|
75
93
|
end
|
76
94
|
|
77
95
|
case response
|
@@ -82,12 +100,17 @@ module Reel
|
|
82
100
|
end
|
83
101
|
|
84
102
|
response.render(@socket)
|
103
|
+
|
104
|
+
# Enable streaming mode
|
105
|
+
if response.headers['Transfer-Encoding'] == "chunked" and response.body.nil?
|
106
|
+
@response_state = :chunked_body
|
107
|
+
end
|
85
108
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
86
109
|
# The client disconnected early
|
87
110
|
@keepalive = false
|
88
111
|
ensure
|
89
112
|
if @keepalive
|
90
|
-
|
113
|
+
reset_request
|
91
114
|
@request_state = :header
|
92
115
|
else
|
93
116
|
@socket.close unless @socket.closed?
|
@@ -95,10 +118,26 @@ module Reel
|
|
95
118
|
end
|
96
119
|
end
|
97
120
|
|
98
|
-
|
99
|
-
|
100
|
-
@
|
101
|
-
|
121
|
+
# Write body chunks directly to the connection
|
122
|
+
def write(chunk)
|
123
|
+
raise StateError, "not in chunked body mode" unless @response_state == :chunked_body
|
124
|
+
chunk_header = chunk.bytesize.to_s(16) + Response::CRLF
|
125
|
+
@socket << chunk_header
|
126
|
+
@socket << chunk
|
127
|
+
end
|
128
|
+
alias_method :<<, :write
|
129
|
+
|
130
|
+
# Finish the response and reset the response state to header
|
131
|
+
def finish_response
|
132
|
+
raise StateError, "not in body state" if @response_state != :chunked_body
|
133
|
+
@socket << "0" << Response::CRLF * 2
|
134
|
+
@response_state = :header
|
135
|
+
end
|
136
|
+
|
137
|
+
# Close the connection
|
138
|
+
def close
|
139
|
+
@keepalive = false
|
140
|
+
@socket.close
|
102
141
|
end
|
103
142
|
end
|
104
143
|
end
|
data/lib/reel/response.rb
CHANGED
@@ -6,17 +6,26 @@ module Reel
|
|
6
6
|
CRLF = "\r\n"
|
7
7
|
|
8
8
|
attr_reader :status # Status has a special setter to coerce symbol names
|
9
|
-
attr_accessor :reason
|
9
|
+
attr_accessor :reason # Reason can be set explicitly if desired
|
10
|
+
attr_reader :headers, :body
|
10
11
|
|
11
12
|
def initialize(status, body_or_headers = nil, body = nil)
|
12
13
|
self.status = status
|
13
14
|
|
14
|
-
if body_or_headers
|
15
|
-
|
16
|
-
@headers = {}
|
17
|
-
else
|
15
|
+
if body_or_headers.is_a?(Hash)
|
16
|
+
headers = body_or_headers
|
18
17
|
@body = body
|
19
|
-
|
18
|
+
else
|
19
|
+
headers = {}
|
20
|
+
@body = body_or_headers
|
21
|
+
end
|
22
|
+
|
23
|
+
@headers = {}
|
24
|
+
headers.each do |name, value|
|
25
|
+
name = name.to_s
|
26
|
+
key = name[Http::CANONICAL_HEADER]
|
27
|
+
key ||= canonicalize_header(name)
|
28
|
+
@headers[key] = value.to_s
|
20
29
|
end
|
21
30
|
|
22
31
|
case @body
|
@@ -24,8 +33,15 @@ module Reel
|
|
24
33
|
@headers['Content-Length'] ||= @body.bytesize
|
25
34
|
when IO
|
26
35
|
@headers['Content-Length'] ||= @body.stat.size
|
36
|
+
when Enumerable
|
37
|
+
@headers['Transfer-Encoding'] ||= 'chunked'
|
38
|
+
when NilClass
|
39
|
+
else raise ArgumentError, "can't render #{@body.class} as a response body"
|
27
40
|
end
|
28
41
|
|
42
|
+
# Prevent modification through the accessor
|
43
|
+
@headers.freeze
|
44
|
+
|
29
45
|
# FIXME: real HTTP versioning
|
30
46
|
@version = "HTTP/1.1"
|
31
47
|
end
|
@@ -59,6 +75,14 @@ module Reel
|
|
59
75
|
while data = @body.read(4096)
|
60
76
|
socket << data
|
61
77
|
end
|
78
|
+
when Enumerable
|
79
|
+
@body.each do |chunk|
|
80
|
+
chunk_header = chunk.bytesize.to_s(16) + CRLF
|
81
|
+
socket << chunk_header
|
82
|
+
socket << chunk
|
83
|
+
end
|
84
|
+
|
85
|
+
socket << "0" << CRLF * 2
|
62
86
|
end
|
63
87
|
end
|
64
88
|
|
@@ -76,5 +100,11 @@ module Reel
|
|
76
100
|
response_header << CRLF
|
77
101
|
end
|
78
102
|
private :render_header
|
103
|
+
|
104
|
+
# Transform to canonical HTTP header capitalization
|
105
|
+
def canonicalize_header(header)
|
106
|
+
header.to_s.split(/[\-_]/).map(&:capitalize).join('-')
|
107
|
+
end
|
108
|
+
private :canonicalize_header
|
79
109
|
end
|
80
110
|
end
|
data/lib/reel/version.rb
CHANGED
@@ -52,6 +52,35 @@ describe Reel::Connection do
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
it "streams responses when transfer-encoding is chunked" do
|
56
|
+
with_socket_pair do |client, connection|
|
57
|
+
client << ExampleRequest.new.to_s
|
58
|
+
request = connection.read_request
|
59
|
+
|
60
|
+
# Sending transfer_encoding chunked without a body enables streaming mode
|
61
|
+
connection.respond :ok, :transfer_encoding => :chunked
|
62
|
+
|
63
|
+
# This will send individual chunks
|
64
|
+
connection << "Hello"
|
65
|
+
connection << "World"
|
66
|
+
connection.finish_response # Write trailer and reset connection to header mode
|
67
|
+
connection.close
|
68
|
+
|
69
|
+
response = ""
|
70
|
+
|
71
|
+
begin
|
72
|
+
while chunk = client.readpartial(4096)
|
73
|
+
response << chunk
|
74
|
+
end
|
75
|
+
rescue EOFError
|
76
|
+
end
|
77
|
+
|
78
|
+
crlf = "\r\n"
|
79
|
+
fixture = "5#{crlf}Hello5#{crlf}World0#{crlf*2}"
|
80
|
+
response[(response.length - fixture.length)..-1].should eq fixture
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
55
84
|
def with_socket_pair
|
56
85
|
host = '127.0.0.1'
|
57
86
|
port = 10103
|
@@ -60,10 +89,12 @@ describe Reel::Connection do
|
|
60
89
|
client = TCPSocket.new(host, port)
|
61
90
|
peer = server.accept
|
62
91
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
92
|
+
begin
|
93
|
+
yield client, Reel::Connection.new(peer)
|
94
|
+
ensure
|
95
|
+
server.close rescue nil
|
96
|
+
client.close rescue nil
|
97
|
+
peer.close rescue nil
|
98
|
+
end
|
68
99
|
end
|
69
100
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Reel::Response do
|
4
|
+
it "streams enumerables" do
|
5
|
+
with_socket_pair do |client, connection|
|
6
|
+
client << ExampleRequest.new.to_s
|
7
|
+
request = connection.read_request
|
8
|
+
|
9
|
+
connection.respond Reel::Response.new(:ok, ["Hello", "World"])
|
10
|
+
|
11
|
+
response = client.readpartial(4096)
|
12
|
+
crlf = "\r\n"
|
13
|
+
fixture = "5#{crlf}Hello5#{crlf}World0#{crlf*2}"
|
14
|
+
response[(response.length - fixture.length)..-1].should eq fixture
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_socket_pair
|
19
|
+
host = '127.0.0.1'
|
20
|
+
port = 10103
|
21
|
+
|
22
|
+
server = TCPServer.new(host, port)
|
23
|
+
client = TCPSocket.new(host, port)
|
24
|
+
peer = server.accept
|
25
|
+
|
26
|
+
begin
|
27
|
+
yield client, Reel::Connection.new(peer)
|
28
|
+
ensure
|
29
|
+
server.close
|
30
|
+
client.close
|
31
|
+
peer.close
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-07-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: celluloid-io
|
16
|
-
requirement: &
|
16
|
+
requirement: &70093521317700 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.8.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70093521317700
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: http
|
27
|
-
requirement: &
|
27
|
+
requirement: &70093521316860 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.2.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70093521316860
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: http_parser.rb
|
38
|
-
requirement: &
|
38
|
+
requirement: &70093521316180 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 0.5.3
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70093521316180
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rake
|
49
|
-
requirement: &
|
49
|
+
requirement: &70093521315560 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70093521315560
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rspec
|
60
|
-
requirement: &
|
60
|
+
requirement: &70093521314860 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,7 +65,7 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70093521314860
|
69
69
|
description: A Celluloid::IO-powered HTTP server
|
70
70
|
email:
|
71
71
|
- tony.arcieri@gmail.com
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- .gitignore
|
78
78
|
- .rspec
|
79
79
|
- .travis.yml
|
80
|
+
- CHANGES.md
|
80
81
|
- Gemfile
|
81
82
|
- LICENSE.txt
|
82
83
|
- README.md
|
@@ -91,7 +92,7 @@ files:
|
|
91
92
|
- lib/reel/connection.rb
|
92
93
|
- lib/reel/logger.rb
|
93
94
|
- lib/reel/request.rb
|
94
|
-
- lib/reel/
|
95
|
+
- lib/reel/request_parser.rb
|
95
96
|
- lib/reel/response.rb
|
96
97
|
- lib/reel/server.rb
|
97
98
|
- lib/reel/version.rb
|
@@ -99,6 +100,7 @@ files:
|
|
99
100
|
- reel.gemspec
|
100
101
|
- spec/fixtures/example.txt
|
101
102
|
- spec/reel/connection_spec.rb
|
103
|
+
- spec/reel/response_spec.rb
|
102
104
|
- spec/reel/server_spec.rb
|
103
105
|
- spec/spec_helper.rb
|
104
106
|
- tasks/rspec.rake
|
@@ -129,6 +131,7 @@ summary: A reel good HTTP server
|
|
129
131
|
test_files:
|
130
132
|
- spec/fixtures/example.txt
|
131
133
|
- spec/reel/connection_spec.rb
|
134
|
+
- spec/reel/response_spec.rb
|
132
135
|
- spec/reel/server_spec.rb
|
133
136
|
- spec/spec_helper.rb
|
134
137
|
has_rdoc:
|