reel 0.2.0 → 0.3.0.pre
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/.travis.yml +2 -3
- data/CHANGES.md +12 -2
- data/README.md +105 -19
- data/bin/reel +2 -1
- data/examples/chunked.rb +25 -0
- data/examples/hello_world.rb +4 -1
- data/examples/roundtrip.rb +157 -0
- data/examples/server-sent-events.rb +48 -0
- data/examples/stream.rb +26 -0
- data/examples/websocket-wall.rb +53 -0
- data/examples/websocket.ru +92 -0
- data/examples/websocket_rack.sh +1 -0
- data/examples/websockets.rb +1 -3
- data/lib/rack/handler/reel.rb +7 -5
- data/lib/reel.rb +6 -0
- data/lib/reel/connection.rb +43 -34
- data/lib/reel/mixins.rb +57 -0
- data/lib/reel/rack_worker.rb +103 -59
- data/lib/reel/request.rb +19 -34
- data/lib/reel/request_parser.rb +12 -2
- data/lib/reel/response.rb +36 -30
- data/lib/reel/server.rb +11 -7
- data/lib/reel/stream.rb +128 -0
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +44 -8
- data/spec/reel/connection_spec.rb +89 -0
- data/spec/reel/rack_worker_spec.rb +28 -16
- data/spec/reel/response_spec.rb +2 -2
- data/spec/reel/server_spec.rb +2 -2
- data/spec/reel/websocket_spec.rb +2 -17
- data/spec/spec_helper.rb +26 -2
- metadata +74 -22
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
0.3.0
|
2
|
+
-----
|
3
|
+
* Reel::App: Sinatra-like DSL for defining Reel apps using Octarine
|
4
|
+
* Chunked upload support
|
5
|
+
* Lots of additional work on the Rack adapter
|
6
|
+
* Expose websockets through Rack as rack.websocket
|
7
|
+
* Performance optimization work
|
8
|
+
* Bugfix: Send CRLF after chunks
|
9
|
+
* Bugfix: Increase TCP connection backlog to 1024
|
10
|
+
|
11
|
+
0.2.0
|
12
|
+
-----
|
3
13
|
* Initial WebSockets support via Reel::WebSocket
|
4
14
|
* Experimental Rack adapter by Alberto Fernández-Capel
|
5
15
|
* Octarine (Sinatra-like DSL) support by Grant Rodgers
|
data/README.md
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
![Reel](https://github.com/celluloid/reel/raw/master/logo.png)
|
2
2
|
=======
|
3
3
|
[![Build Status](https://secure.travis-ci.org/celluloid/reel.png?branch=master)](http://travis-ci.org/celluloid/reel)
|
4
|
+
[![Dependency Status](https://gemnasium.com/celluloid/reel.png)](https://gemnasium.com/celluloid/reel)
|
5
|
+
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/celluloid/reel)
|
4
6
|
|
5
7
|
Reel is a fast, non-blocking "evented" web server built on [http_parser.rb][parser],
|
6
|
-
[
|
8
|
+
[websocket_parser][websockets], [Celluloid::IO][celluloidio], and [nio4r][nio4r]. Thanks
|
7
9
|
to Celluloid, Reel also works great for multithreaded applications and provides
|
8
10
|
traditional multithreaded blocking I/O support too.
|
9
11
|
|
10
12
|
[parser]: https://github.com/tmm1/http_parser.rb
|
11
|
-
[websockets]: https://github.com/
|
13
|
+
[websockets]: https://github.com/afcapel/websocket_parser
|
12
14
|
[celluloidio]: https://github.com/celluloid/celluloid-io
|
13
15
|
[nio4r]: https://github.com/tarcieri/nio4r
|
14
16
|
|
@@ -24,9 +26,10 @@ primarily I/O bound, and threads for where you're compute bound.
|
|
24
26
|
|
25
27
|
### Is it any good?
|
26
28
|
|
27
|
-
[Yes](http://news.ycombinator.com/item?id=3067434)
|
28
|
-
|
29
|
-
run on a 2GHz i7 (OS X 10.7.3).
|
29
|
+
[Yes](http://news.ycombinator.com/item?id=3067434)
|
30
|
+
|
31
|
+
Here's a "hello world" web server benchmark, run on a 2GHz i7 (OS X 10.7.3).
|
32
|
+
All servers used in a single-threaded mode.
|
30
33
|
|
31
34
|
Reel performance on various Ruby VMs:
|
32
35
|
|
@@ -35,10 +38,9 @@ Reel performance on various Ruby VMs:
|
|
35
38
|
|
36
39
|
Ruby Version Throughput Latency
|
37
40
|
------------ ---------- -------
|
38
|
-
JRuby
|
39
|
-
Ruby 1.9.3 5263 reqs/s (0.2 ms/req)
|
40
|
-
JRuby 1.6.7 4303 reqs/s (0.2 ms/req)
|
41
|
+
JRuby 1.7.0 3978 req/s (0.3 ms/req)
|
41
42
|
rbx HEAD 2288 reqs/s (0.4 ms/req)
|
43
|
+
Ruby 1.9.3 2071 req/s (0.5 ms/req)
|
42
44
|
```
|
43
45
|
|
44
46
|
Comparison with other web servers:
|
@@ -54,10 +56,15 @@ Node.js (0.6.5) 11735 reqs/s (0.1 ms/req)
|
|
54
56
|
All Ruby benchmarks done on Ruby 1.9.3. Latencies given are average-per-request
|
55
57
|
and are not amortized across all concurrent requests.
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
+
API
|
60
|
+
---
|
61
|
+
|
62
|
+
Reel also provides a "bare metal" API which was used in the benchmarks above.
|
63
|
+
Here are some examples:
|
64
|
+
|
65
|
+
### Block Form
|
59
66
|
|
60
|
-
Reel
|
67
|
+
Reel lets you pass a block to initialize which receives connections:
|
61
68
|
|
62
69
|
```ruby
|
63
70
|
require 'reel'
|
@@ -67,11 +74,11 @@ Reel::Server.supervise("0.0.0.0", 3000) do |connection|
|
|
67
74
|
case request
|
68
75
|
when Reel::Request
|
69
76
|
puts "Client requested: #{request.method} #{request.url}"
|
70
|
-
|
77
|
+
request.respond :ok, "Hello, world!"
|
71
78
|
when Reel::WebSocket
|
72
79
|
puts "Client made a WebSocket request to: #{request.url}"
|
73
|
-
request << "Hello there"
|
74
|
-
|
80
|
+
request << "Hello everyone out there in WebSocket land"
|
81
|
+
request.close
|
75
82
|
break
|
76
83
|
end
|
77
84
|
end
|
@@ -82,12 +89,91 @@ When we read a request from the incoming connection, we'll either get back
|
|
82
89
|
a Reel::Request object, indicating a normal HTTP connection, or a
|
83
90
|
Reel::WebSocket object for WebSockets connections.
|
84
91
|
|
85
|
-
|
86
|
-
|
92
|
+
### Subclass Form
|
93
|
+
|
94
|
+
You can also subclass Reel, which allows additional customizations:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
require 'reel'
|
98
|
+
|
99
|
+
class MyServer < Reel::Server
|
100
|
+
def initialize(host = "127.0.0.1", port = 3000)
|
101
|
+
super(host, port, &method(:on_connection))
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_connection(connection)
|
105
|
+
while request = connection.request
|
106
|
+
case request
|
107
|
+
when Reel::Request
|
108
|
+
handle_request(request)
|
109
|
+
when Reel::WebSocket
|
110
|
+
handle_websocket(request)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def handle_request(request)
|
116
|
+
request.respond :ok, "Hello, world!"
|
117
|
+
end
|
118
|
+
|
119
|
+
def handle_websocket(sock)
|
120
|
+
sock << "Hello everyone out there in WebSocket land!"
|
121
|
+
sock.close
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
MyServer.run
|
126
|
+
```
|
127
|
+
|
128
|
+
Framework Adapters
|
129
|
+
------------------
|
130
|
+
### Rack
|
131
|
+
|
132
|
+
Reel can be used as a standard Rack server via the "reel" command line
|
133
|
+
application. Please be aware that Rack support is experimental and that there
|
134
|
+
are potential complications between using large numbers of rack middlewares
|
135
|
+
and the limited 4kB stack depth of Ruby Fibers, which are used extensively
|
136
|
+
by Celluloid. In addition, the Rack specification mandates that request bodies
|
137
|
+
are rewindable, which prevents streaming request bodies as the spec dictates
|
138
|
+
they must be written to disk.
|
139
|
+
|
140
|
+
To really leverage Reel's capabilities, you must use Reel via its own API,
|
141
|
+
or another Ruby library with direct Reel support.
|
142
|
+
|
143
|
+
### Webmachine
|
144
|
+
|
145
|
+
The most notable library with native Reel support is
|
146
|
+
[webmachine-ruby](https://github.com/seancribbs/webmachine-ruby),
|
147
|
+
an advanced HTTP framework for Ruby with a complete state machine for proper
|
148
|
+
processing of HTTP/1.1 requests. Together with Reel, Webmachine provides
|
149
|
+
full streaming support for both requests and responses.
|
150
|
+
|
151
|
+
To use Reel with Webmachine, add the following to your Gemfile:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
gem 'webmachine', git: 'git://github.com/seancribbs/webmachine-ruby.git'
|
155
|
+
```
|
156
|
+
|
157
|
+
Then use `config.adapter = :Reel` when configuring a Webmachine app, e.g:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
MyApp = Webmachine::Application.new do |app|
|
161
|
+
app.routes do
|
162
|
+
add ['*'], MyHome
|
163
|
+
end
|
164
|
+
|
165
|
+
app.configure do |config|
|
166
|
+
config.ip = MYAPP_IP
|
167
|
+
config.port = MYAPP_PORT
|
168
|
+
config.adapter = :Reel
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
MyApp.run
|
173
|
+
```
|
87
174
|
|
88
|
-
|
89
|
-
|
90
|
-
speaking HTTP and has basic keep-alive support.
|
175
|
+
See the [Webmachine documentation](http://rubydoc.info/gems/webmachine/frames/file/README.md)
|
176
|
+
for further information
|
91
177
|
|
92
178
|
Contributing
|
93
179
|
------------
|
data/bin/reel
CHANGED
data/examples/chunked.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'reel'
|
4
|
+
|
5
|
+
app = Rack::Builder.new do
|
6
|
+
map '/' do
|
7
|
+
run lambda { |env|
|
8
|
+
body = Reel::ChunkStream.new do |body|
|
9
|
+
# sending a payload to make sure browsers will render chunks as received
|
10
|
+
body << "<html>#{' '*1024}\n"
|
11
|
+
('A'..'Z').each do |l|
|
12
|
+
body << "<div>#{l}</div>\n"
|
13
|
+
sleep 0.5
|
14
|
+
end
|
15
|
+
body << "</html>\n"
|
16
|
+
body.finish
|
17
|
+
end
|
18
|
+
[200, {
|
19
|
+
'Content-Type' => 'text/html'
|
20
|
+
}, body]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end.to_app
|
24
|
+
|
25
|
+
Rack::Handler::Reel.run app, Port: 9292
|
data/examples/hello_world.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/hello_world.rb
|
3
|
+
|
1
4
|
require 'rubygems'
|
2
5
|
require 'bundler/setup'
|
3
6
|
require 'reel'
|
@@ -5,7 +8,7 @@ require 'reel'
|
|
5
8
|
addr, port = '127.0.0.1', 1234
|
6
9
|
|
7
10
|
puts "*** Starting server on #{addr}:#{port}"
|
8
|
-
Reel::Server.
|
11
|
+
Reel::Server.run(addr, port) do |connection|
|
9
12
|
# To use keep-alive with Reel, use a while loop that repeatedly calls
|
10
13
|
# connection.request and consumes connection objects
|
11
14
|
while request = connection.request
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'reel'
|
4
|
+
|
5
|
+
|
6
|
+
class RoundtripServer
|
7
|
+
include Celluloid
|
8
|
+
include Celluloid::Notifications
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
async.run
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
now = Time.now.to_f
|
16
|
+
sleep now.ceil - now + 0.001
|
17
|
+
every(1) do
|
18
|
+
publish 'read_message'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Writer
|
24
|
+
include Celluloid
|
25
|
+
include Celluloid::Notifications
|
26
|
+
include Celluloid::Logger
|
27
|
+
|
28
|
+
def initialize(websocket)
|
29
|
+
info "Writing to socket"
|
30
|
+
@socket = websocket
|
31
|
+
subscribe('write_message', :new_message)
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_message(topic, new_time)
|
35
|
+
@socket << new_time.inspect
|
36
|
+
rescue Reel::SocketError
|
37
|
+
info "WS client disconnected"
|
38
|
+
terminate
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class Reader
|
44
|
+
include Celluloid
|
45
|
+
include Celluloid::Notifications
|
46
|
+
include Celluloid::Logger
|
47
|
+
|
48
|
+
def initialize(websocket)
|
49
|
+
info "Reading socket"
|
50
|
+
@socket = websocket
|
51
|
+
subscribe('read_message', :new_message)
|
52
|
+
end
|
53
|
+
|
54
|
+
def new_message(topic)
|
55
|
+
msg = @socket.read
|
56
|
+
publish 'write_message', msg
|
57
|
+
rescue Reel::SocketError, EOFError
|
58
|
+
info "WS client disconnected"
|
59
|
+
terminate
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class WebServer < Reel::Server
|
64
|
+
include Celluloid::Logger
|
65
|
+
|
66
|
+
def initialize(host = "0.0.0.0", port = 9000)
|
67
|
+
info "Roundtrip example starting on #{host}:#{port}"
|
68
|
+
super(host, port, &method(:on_connection))
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_connection(connection)
|
72
|
+
while request = connection.request
|
73
|
+
case request
|
74
|
+
when Reel::Request
|
75
|
+
route_request connection, request
|
76
|
+
when Reel::WebSocket
|
77
|
+
info "Received a WebSocket connection"
|
78
|
+
route_websocket request
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def route_request(connection, request)
|
84
|
+
if request.url == "/"
|
85
|
+
return render_index(connection)
|
86
|
+
end
|
87
|
+
|
88
|
+
info "404 Not Found: #{request.path}"
|
89
|
+
connection.respond :not_found, "Not found"
|
90
|
+
end
|
91
|
+
|
92
|
+
def route_websocket(socket)
|
93
|
+
if socket.url == "/ws"
|
94
|
+
Writer.new(socket)
|
95
|
+
Reader.new(socket)
|
96
|
+
else
|
97
|
+
info "Received invalid WebSocket request for: #{socket.url}"
|
98
|
+
socket.close
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def render_index(connection)
|
103
|
+
info "200 OK: /"
|
104
|
+
connection.respond :ok, <<-HTML
|
105
|
+
<!doctype html>
|
106
|
+
<html lang="en">
|
107
|
+
<head>
|
108
|
+
<meta charset="utf-8">
|
109
|
+
<title>Reel WebSockets roundtrip example</title>
|
110
|
+
<style>
|
111
|
+
body {
|
112
|
+
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
113
|
+
font-weight: 300;
|
114
|
+
text-align: center;
|
115
|
+
}
|
116
|
+
|
117
|
+
#content {
|
118
|
+
width: 800px;
|
119
|
+
margin: 0 auto;
|
120
|
+
background: #EEEEEE;
|
121
|
+
padding: 1em;
|
122
|
+
}
|
123
|
+
</style>
|
124
|
+
</head>
|
125
|
+
<body>
|
126
|
+
<div id="content">
|
127
|
+
<h1>Roundtrip communication with websockets</h1>
|
128
|
+
<div>
|
129
|
+
<input id="text_input" type="text" name="q" value="" autocomplete="off"/>
|
130
|
+
Latest message is: <span id="current-time">...</span></div>
|
131
|
+
</div>
|
132
|
+
</body>
|
133
|
+
<script>
|
134
|
+
var SocketKlass = "MozWebSocket" in window ? MozWebSocket : WebSocket;
|
135
|
+
var ws = new SocketKlass('ws://' + window.location.host + '/ws');
|
136
|
+
ws.onmessage = function(msg){
|
137
|
+
document.getElementById('current-time').innerHTML = msg.data;
|
138
|
+
};
|
139
|
+
var input = document.getElementById("text_input");
|
140
|
+
input.focus();
|
141
|
+
input.onkeydown = function(evt) {
|
142
|
+
var evt = evt || window.event;
|
143
|
+
if (evt.keyCode === 13) {
|
144
|
+
ws.send(input.value);console.log(input.value);
|
145
|
+
input.value = "";
|
146
|
+
}
|
147
|
+
};
|
148
|
+
</script>
|
149
|
+
</html>
|
150
|
+
HTML
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
RoundtripServer.supervise_as :roundtrip_server
|
155
|
+
WebServer.supervise_as :reel
|
156
|
+
|
157
|
+
sleep
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'reel'
|
4
|
+
|
5
|
+
Connections = []
|
6
|
+
Body = DATA.read
|
7
|
+
app = Rack::Builder.new do
|
8
|
+
map '/' do
|
9
|
+
run lambda { |env|
|
10
|
+
[200, {'Content-Type' => 'text/html'}, [Body]]
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
map '/subscribe' do
|
15
|
+
run lambda { |env|
|
16
|
+
body = Reel::EventStream.new do |socket|
|
17
|
+
Connections << socket
|
18
|
+
socket.on_error { Connections.delete socket }
|
19
|
+
end
|
20
|
+
[200, {'Content-Type' => 'text/event-stream'}, body]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
map '/wall' do
|
25
|
+
run lambda { |env|
|
26
|
+
msg = env['PATH_INFO'].gsub(/\/+/, '').strip
|
27
|
+
msg = Time.now if msg.empty?
|
28
|
+
Connections.each { |s| s.data msg }
|
29
|
+
[200, {'Content-Type' => 'text/html'}, ["Sent \"#{msg}\" to #{Connections.size} clients"]]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end.to_app
|
33
|
+
|
34
|
+
Rack::Handler::Reel.run app, Port: 9292
|
35
|
+
|
36
|
+
__END__
|
37
|
+
<!doctype html>
|
38
|
+
<html lang="en">
|
39
|
+
<body>
|
40
|
+
<div id="content">Waiting for messages...</div>
|
41
|
+
</body>
|
42
|
+
<script type="text/javascript">
|
43
|
+
var evs = new EventSource('/subscribe');
|
44
|
+
evs.onmessage = function(e){
|
45
|
+
document.getElementById('content').innerHTML = e.data;
|
46
|
+
}
|
47
|
+
</script>
|
48
|
+
</html>
|