reel 0.4.0.pre → 0.4.0.pre2
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.
- checksums.yaml +4 -4
- data/CHANGES.md +11 -3
- data/Gemfile +4 -0
- data/Guardfile +24 -0
- data/README.md +26 -16
- data/examples/ssl_hello_world.rb +27 -0
- data/examples/websocket_rack.sh +1 -1
- data/examples/websockets.rb +15 -5
- data/lib/rack/handler/reel.rb +1 -0
- data/lib/reel.rb +12 -1
- data/lib/reel/app.rb +1 -1
- data/lib/reel/connection.rb +59 -56
- data/lib/reel/mixins.rb +13 -9
- data/lib/reel/rack_worker.rb +3 -3
- data/lib/reel/request.rb +72 -34
- data/lib/reel/request_body.rb +56 -0
- data/lib/reel/request_info.rb +27 -0
- data/lib/reel/request_parser.rb +42 -30
- data/lib/reel/response.rb +5 -49
- data/lib/reel/response_writer.rb +69 -0
- data/lib/reel/server.rb +6 -6
- data/lib/reel/ssl_server.rb +39 -0
- data/lib/reel/stream.rb +5 -6
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +6 -4
- data/reel.gemspec +4 -3
- data/spec/fixtures/client.crt +22 -0
- data/spec/fixtures/client.key +27 -0
- data/spec/fixtures/server.crt +22 -0
- data/spec/fixtures/server.key +27 -0
- data/spec/reel/connection_spec.rb +233 -7
- data/spec/reel/response_spec.rb +1 -1
- data/spec/reel/server_spec.rb +1 -1
- data/spec/reel/ssl_server_spec.rb +54 -0
- data/spec/reel/websocket_spec.rb +34 -2
- data/spec/spec_helper.rb +11 -5
- metadata +38 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c139c2fa0dcd5e9456cb8867fbe296eddc1c9f6
|
4
|
+
data.tar.gz: 0ba97001f92ac212ba4184ba5059580e0e124245
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02cfe9e2b456d5f6abb5c7810df4d6133ef5a1acfb49917de65f245d6bfadfe48c525b527ed060348ff8b306c071ac6714fa274c47aef693e08d79330aa639d9
|
7
|
+
data.tar.gz: a9ebba27cce7485bae2ae2236311d5fe43e59fc8e2d03c2d3b5a2f2ca4b728b6cccf430b6962d6abad639b614da846b1367c0880095cae2e420cbdf91bbefedf
|
data/CHANGES.md
CHANGED
@@ -1,6 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
*
|
1
|
+
0.4.0.pre2
|
2
|
+
----------
|
3
|
+
* Pipelining support
|
4
|
+
* Reel::Connection#each_request for iterating through keep-alive requests
|
5
|
+
* Reel::Request#body now returns a Reel::RequestBody object instead of a String
|
6
|
+
* New WebSocket API: obtain WebSockets through Reel::Request#websocket instead
|
7
|
+
of through Reel::Connection#request. Allows processing of WebSockets through
|
8
|
+
other means than the built-in WebSocket support
|
9
|
+
* Allow Reel to stop cleanly
|
10
|
+
* Remove `on_error` callback system
|
11
|
+
* Increase buffer size
|
4
12
|
|
5
13
|
0.3.0
|
6
14
|
-----
|
data/Gemfile
CHANGED
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara features specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
[![Code Climate](https://codeclimate.com/github/celluloid/reel.png)](https://codeclimate.com/github/celluloid/reel)
|
7
7
|
[![Coverage Status](https://coveralls.io/repos/celluloid/reel/badge.png?branch=master)](https://coveralls.io/r/celluloid/reel)
|
8
8
|
|
9
|
-
> "A dizzying lifetime...
|
9
|
+
> "A dizzying lifetime... reeling by on Celluloid" _-- Rush / Between The Wheels_
|
10
10
|
|
11
11
|
Reel is a fast, non-blocking "evented" web server built on [http_parser.rb][parser],
|
12
12
|
[websocket_parser][websockets], [Celluloid::IO][celluloidio], and [nio4r][nio4r]. Thanks
|
@@ -16,7 +16,7 @@ traditional multithreaded blocking I/O support too.
|
|
16
16
|
[parser]: https://github.com/tmm1/http_parser.rb
|
17
17
|
[websockets]: https://github.com/afcapel/websocket_parser
|
18
18
|
[celluloidio]: https://github.com/celluloid/celluloid-io
|
19
|
-
[nio4r]: https://github.com/
|
19
|
+
[nio4r]: https://github.com/celluloid/nio4r
|
20
20
|
|
21
21
|
Connections to Reel can be either non-blocking and handled entirely within
|
22
22
|
the Reel::Server thread, or the same connections can be dispatched to worker
|
@@ -63,6 +63,8 @@ and are not amortized across all concurrent requests.
|
|
63
63
|
API
|
64
64
|
---
|
65
65
|
|
66
|
+
*NOTE: these examples are for the Reel 0.4.0.pre2 API*
|
67
|
+
|
66
68
|
Reel also provides a "bare metal" API which was used in the benchmarks above.
|
67
69
|
Here are some examples:
|
68
70
|
|
@@ -74,16 +76,18 @@ Reel lets you pass a block to initialize which receives connections:
|
|
74
76
|
require 'reel'
|
75
77
|
|
76
78
|
Reel::Server.supervise("0.0.0.0", 3000) do |connection|
|
77
|
-
|
78
|
-
|
79
|
-
|
79
|
+
# Support multiple keep-alive requests per connection
|
80
|
+
connection.each_request do |request|
|
81
|
+
# WebSocket support
|
82
|
+
if request.websocket?
|
83
|
+
puts "Client made a WebSocket request to: #{request.url}"
|
84
|
+
websocket = request.websocket
|
85
|
+
|
86
|
+
websocket << "Hello everyone out there in WebSocket land"
|
87
|
+
websocket.close
|
88
|
+
else
|
80
89
|
puts "Client requested: #{request.method} #{request.url}"
|
81
90
|
request.respond :ok, "Hello, world!"
|
82
|
-
when Reel::WebSocket
|
83
|
-
puts "Client made a WebSocket request to: #{request.url}"
|
84
|
-
request << "Hello everyone out there in WebSocket land"
|
85
|
-
request.close
|
86
|
-
break
|
87
91
|
end
|
88
92
|
end
|
89
93
|
end
|
@@ -108,12 +112,11 @@ class MyServer < Reel::Server
|
|
108
112
|
end
|
109
113
|
|
110
114
|
def on_connection(connection)
|
111
|
-
|
112
|
-
|
113
|
-
when Reel::Request
|
114
|
-
handle_request(request)
|
115
|
-
when Reel::WebSocket
|
115
|
+
connection.each_request do |request|
|
116
|
+
if request.websocket?
|
116
117
|
handle_websocket(request)
|
118
|
+
else
|
119
|
+
handle_request(request)
|
117
120
|
end
|
118
121
|
end
|
119
122
|
end
|
@@ -133,6 +136,7 @@ MyServer.run
|
|
133
136
|
|
134
137
|
Framework Adapters
|
135
138
|
------------------
|
139
|
+
|
136
140
|
### Rack
|
137
141
|
|
138
142
|
Reel can be used as a standard Rack server via the "reel" command line
|
@@ -143,6 +147,12 @@ by Celluloid. In addition, the Rack specification mandates that request bodies
|
|
143
147
|
are rewindable, which prevents streaming request bodies as the spec dictates
|
144
148
|
they must be written to disk.
|
145
149
|
|
150
|
+
To run `.ru` file using Reel w/ 16 workers
|
151
|
+
|
152
|
+
```
|
153
|
+
rackup -p 1234 -s reel config.ru -Enone -O "workers=16"
|
154
|
+
```
|
155
|
+
|
146
156
|
To really leverage Reel's capabilities, you must use Reel via its own API,
|
147
157
|
or another Ruby library with direct Reel support.
|
148
158
|
|
@@ -173,7 +183,7 @@ MyApp = Webmachine::Application.new do |app|
|
|
173
183
|
config.port = MYAPP_PORT
|
174
184
|
config.adapter = :Reel
|
175
185
|
|
176
|
-
# Optional:
|
186
|
+
# Optional: handler for incoming websockets
|
177
187
|
config.adapter_options[:websocket_handler] = proc do |websocket|
|
178
188
|
# socket is a Reel::WebSocket
|
179
189
|
socket << "hello, world"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/hello_world.rb
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'reel'
|
7
|
+
|
8
|
+
addr, port = '127.0.0.1', 4430
|
9
|
+
options = {
|
10
|
+
:cert => File.read(File.expand_path("../../spec/fixtures/server.crt", __FILE__)),
|
11
|
+
:key => File.read(File.expand_path("../../spec/fixtures/server.key", __FILE__))
|
12
|
+
}
|
13
|
+
|
14
|
+
puts "*** Starting server on #{addr}:#{port}"
|
15
|
+
Reel::SSLServer.supervise(addr, port, options) do |connection|
|
16
|
+
# To use keep-alive with Reel, use a while loop that repeatedly calls
|
17
|
+
# connection.request and consumes connection objects
|
18
|
+
while request = connection.request
|
19
|
+
# Ordinarily we'd route the request here, e.g.
|
20
|
+
# route request.url
|
21
|
+
connection.respond :ok, "hello, world!"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reel takes care of closing the connection
|
25
|
+
end
|
26
|
+
|
27
|
+
sleep
|
data/examples/websocket_rack.sh
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rackup -s reel websocket.ru -Enone
|
1
|
+
rackup -s reel websocket.ru -Enone -O "workers=16"
|
data/examples/websockets.rb
CHANGED
@@ -48,12 +48,22 @@ class WebServer < Reel::Server
|
|
48
48
|
|
49
49
|
def on_connection(connection)
|
50
50
|
while request = connection.request
|
51
|
-
|
52
|
-
when Reel::Request
|
53
|
-
route_request connection, request
|
54
|
-
when Reel::WebSocket
|
51
|
+
if request.websocket?
|
55
52
|
info "Received a WebSocket connection"
|
56
|
-
|
53
|
+
|
54
|
+
# We're going to hand off this connection to another actor (TimeClient)
|
55
|
+
# However, initially Reel::Connections are "attached" to the
|
56
|
+
# Reel::Server actor, meaning that the server manages the connection
|
57
|
+
# lifecycle (e.g. error handling) for us.
|
58
|
+
#
|
59
|
+
# If we want to hand this connection off to another actor, we first
|
60
|
+
# need to detach it from the Reel::Server
|
61
|
+
connection.detach
|
62
|
+
|
63
|
+
route_websocket request.websocket
|
64
|
+
return
|
65
|
+
else
|
66
|
+
route_request connection, request
|
57
67
|
end
|
58
68
|
end
|
59
69
|
end
|
data/lib/rack/handler/reel.rb
CHANGED
@@ -83,6 +83,7 @@ module Rack
|
|
83
83
|
options = options.inject({}) { |h, (k,v)| h[k.downcase] = v ; h }
|
84
84
|
options[:rackup] = options[:config] if options[:config]
|
85
85
|
options[:port] = options[:port].to_i if options[:port]
|
86
|
+
options[:workers] = options[:workers].to_i if options[:workers]
|
86
87
|
options
|
87
88
|
end
|
88
89
|
end
|
data/lib/reel.rb
CHANGED
@@ -11,10 +11,14 @@ require 'reel/version'
|
|
11
11
|
require 'reel/mixins'
|
12
12
|
require 'reel/connection'
|
13
13
|
require 'reel/logger'
|
14
|
+
require 'reel/request_info'
|
14
15
|
require 'reel/request'
|
16
|
+
require 'reel/request_body'
|
15
17
|
require 'reel/request_parser'
|
16
18
|
require 'reel/response'
|
19
|
+
require 'reel/response_writer'
|
17
20
|
require 'reel/server'
|
21
|
+
require 'reel/ssl_server'
|
18
22
|
require 'reel/websocket'
|
19
23
|
require 'reel/stream'
|
20
24
|
|
@@ -29,9 +33,16 @@ module Reel
|
|
29
33
|
# Error reading a request
|
30
34
|
class RequestError < StandardError; end
|
31
35
|
|
32
|
-
# Error
|
36
|
+
# Error occurred performing IO on a socket
|
33
37
|
class SocketError < RequestError; end
|
34
38
|
|
39
|
+
# Error occurred when trying to use the socket after it was upgraded
|
40
|
+
class SocketUpgradedError < NilClass
|
41
|
+
def self.method_missing(m, *)
|
42
|
+
raise(Reel::RequestError, 'Reel::Connection#socket can not be used anymore as it was upgraded. Use Reel::WebSocket instance instead.')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
35
46
|
# Error occured during a WebSockets handshake
|
36
47
|
class HandshakeError < RequestError; end
|
37
48
|
|
data/lib/reel/app.rb
CHANGED
@@ -16,7 +16,7 @@ module Reel
|
|
16
16
|
super()
|
17
17
|
@server = Reel::Server.supervise(host, port) do |connection|
|
18
18
|
while request = connection.request
|
19
|
-
status, headers, body = call Rack::MockRequest.env_for(request.url, :method => request.method, :input => request.body)
|
19
|
+
status, headers, body = call Rack::MockRequest.env_for(request.url, :method => request.method, :input => request.body.to_s)
|
20
20
|
response_klass = body.is_a?(Stream) ? StreamResponse : Response
|
21
21
|
connection.respond(response_klass.new(status_symbol(status), headers, body))
|
22
22
|
end
|
data/lib/reel/connection.rb
CHANGED
@@ -10,18 +10,20 @@ module Reel
|
|
10
10
|
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
11
11
|
KEEP_ALIVE = 'Keep-Alive'.freeze
|
12
12
|
CLOSE = 'close'.freeze
|
13
|
-
CHUNKED = 'chunked'.freeze
|
14
13
|
|
15
14
|
attr_reader :socket, :parser
|
16
15
|
|
17
16
|
# Attempt to read this much data
|
18
17
|
BUFFER_SIZE = 16384
|
18
|
+
attr_reader :buffer_size
|
19
19
|
|
20
|
-
def initialize(socket)
|
20
|
+
def initialize(socket, buffer_size = nil)
|
21
21
|
@attached = true
|
22
22
|
@socket = socket
|
23
23
|
@keepalive = true
|
24
|
-
@parser = Request::Parser.new
|
24
|
+
@parser = Request::Parser.new(socket, self)
|
25
|
+
@writer = Response::Writer.new(socket, self)
|
26
|
+
@buffer_size = buffer_size.nil? ? BUFFER_SIZE : buffer_size
|
25
27
|
reset_request
|
26
28
|
|
27
29
|
@response_state = :header
|
@@ -40,26 +42,29 @@ module Reel
|
|
40
42
|
end
|
41
43
|
|
42
44
|
# Reset the current request state
|
43
|
-
def reset_request(state = :
|
45
|
+
def reset_request(state = :ready)
|
44
46
|
@request_state = state
|
45
|
-
@
|
47
|
+
@current_request = nil
|
46
48
|
@parser.reset
|
47
49
|
end
|
48
50
|
|
51
|
+
def readpartial(size = @buffer_size)
|
52
|
+
raise StateError, "can't read in the '#{@request_state}' request state" unless @request_state == :ready
|
53
|
+
@parser.readpartial(size)
|
54
|
+
end
|
55
|
+
|
56
|
+
def current_request
|
57
|
+
@current_request
|
58
|
+
end
|
59
|
+
|
49
60
|
# Read a request object from the connection
|
50
61
|
def request
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
|
58
|
-
when WebSocket
|
59
|
-
@request_state = @response_state = :websocket
|
60
|
-
@socket = nil
|
61
|
-
else raise "unexpected request type: #{req.class}"
|
62
|
-
end
|
62
|
+
raise StateError, "already processing a request" if current_request
|
63
|
+
|
64
|
+
req = @parser.current_request
|
65
|
+
@request_state = :ready
|
66
|
+
@keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
|
67
|
+
@current_request = req
|
63
68
|
|
64
69
|
req
|
65
70
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
@@ -69,45 +74,23 @@ module Reel
|
|
69
74
|
nil
|
70
75
|
end
|
71
76
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
77
|
+
# Enumerate the requests from this connection, since we might receive
|
78
|
+
# many if the client is using keep-alive
|
79
|
+
def each_request
|
80
|
+
while req = request
|
81
|
+
yield req
|
75
82
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
83
|
+
# Websockets upgrade the connection to the Websocket protocol
|
84
|
+
# Once we have finished processing a Websocket, we can't handle
|
85
|
+
# additional requests
|
86
|
+
break if req.websocket?
|
80
87
|
end
|
81
|
-
|
82
|
-
chunk
|
83
|
-
end
|
84
|
-
|
85
|
-
# read length bytes from request body
|
86
|
-
def read(length = nil, buffer = nil)
|
87
|
-
raise ArgumentError, "negative length #{length} given" if length && length < 0
|
88
|
-
|
89
|
-
return '' if length == 0
|
90
|
-
|
91
|
-
res = buffer.nil? ? '' : buffer.clear
|
92
|
-
|
93
|
-
chunk_size = length.nil? ? BUFFER_SIZE : length
|
94
|
-
begin
|
95
|
-
while chunk_size > 0
|
96
|
-
chunk = readpartial(chunk_size)
|
97
|
-
break unless chunk
|
98
|
-
res << chunk
|
99
|
-
chunk_size = length - res.length unless length.nil?
|
100
|
-
end
|
101
|
-
rescue EOFError
|
102
|
-
end
|
103
|
-
|
104
|
-
return length && res.length == 0 ? nil : res
|
105
88
|
end
|
106
89
|
|
107
90
|
# Send a response back to the client
|
108
91
|
# Response can be a symbol indicating the status code or a Reel::Response
|
109
92
|
def respond(response, headers_or_body = {}, body = nil)
|
110
|
-
raise StateError "not in header state" if @response_state != :header
|
93
|
+
raise StateError, "not in header state" if @response_state != :header
|
111
94
|
|
112
95
|
if headers_or_body.is_a? Hash
|
113
96
|
headers = headers_or_body
|
@@ -129,10 +112,10 @@ module Reel
|
|
129
112
|
else raise TypeError, "invalid response: #{response.inspect}"
|
130
113
|
end
|
131
114
|
|
132
|
-
|
115
|
+
@writer.handle_response(response)
|
133
116
|
|
134
117
|
# Enable streaming mode
|
135
|
-
if response.
|
118
|
+
if response.chunked? and response.body.nil?
|
136
119
|
@response_state = :chunked_body
|
137
120
|
end
|
138
121
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
@@ -140,7 +123,7 @@ module Reel
|
|
140
123
|
@keepalive = false
|
141
124
|
ensure
|
142
125
|
if @keepalive
|
143
|
-
reset_request(:
|
126
|
+
reset_request(:ready)
|
144
127
|
else
|
145
128
|
@socket.close unless @socket.closed?
|
146
129
|
reset_request(:closed)
|
@@ -150,23 +133,43 @@ module Reel
|
|
150
133
|
# Write body chunks directly to the connection
|
151
134
|
def write(chunk)
|
152
135
|
raise StateError, "not in chunked body mode" unless @response_state == :chunked_body
|
153
|
-
|
154
|
-
@socket << chunk_header + Response::CRLF
|
155
|
-
@socket << chunk + Response::CRLF
|
136
|
+
@writer.write(chunk)
|
156
137
|
end
|
157
138
|
alias_method :<<, :write
|
158
139
|
|
159
140
|
# Finish the response and reset the response state to header
|
160
141
|
def finish_response
|
161
142
|
raise StateError, "not in body state" if @response_state != :chunked_body
|
162
|
-
@
|
143
|
+
@writer.finish_response
|
163
144
|
@response_state = :header
|
164
145
|
end
|
165
146
|
|
166
147
|
# Close the connection
|
167
148
|
def close
|
149
|
+
raise StateError, "socket has been hijacked from this connection" unless @socket
|
150
|
+
|
168
151
|
@keepalive = false
|
169
152
|
@socket.close unless @socket.closed?
|
170
153
|
end
|
154
|
+
|
155
|
+
# Hijack the socket from the connection
|
156
|
+
def hijack_socket
|
157
|
+
# FIXME: this doesn't do a great job of ensuring we can hijack the socket
|
158
|
+
# in its current state. Improve the state detection.
|
159
|
+
if @request_state != :ready && @response_state != :header
|
160
|
+
raise StateError, "connection is not in a hijackable state"
|
161
|
+
end
|
162
|
+
|
163
|
+
@request_state = @response_state = :hijacked
|
164
|
+
socket = @socket
|
165
|
+
@socket = nil
|
166
|
+
socket
|
167
|
+
end
|
168
|
+
|
169
|
+
# Raw access to the underlying socket
|
170
|
+
def socket
|
171
|
+
raise StateError, "socket has already been hijacked" unless @socket
|
172
|
+
@socket
|
173
|
+
end
|
171
174
|
end
|
172
175
|
end
|