reel 0.1.0 → 0.2.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 -5
- data/CHANGES.md +6 -2
- data/README.md +19 -7
- data/bin/reel +49 -5
- data/examples/hello_world.rb +9 -3
- data/examples/websockets.rb +124 -0
- data/lib/rack/handler/reel.rb +84 -0
- data/lib/reel.rb +15 -0
- data/lib/reel/connection.rb +47 -32
- data/lib/reel/rack_worker.rb +90 -0
- data/lib/reel/request.rb +46 -8
- data/lib/reel/response.rb +8 -4
- data/lib/reel/server.rb +7 -4
- data/lib/reel/version.rb +1 -1
- data/lib/reel/websocket.rb +64 -0
- data/reel.gemspec +7 -5
- data/spec/reel/connection_spec.rb +14 -17
- data/spec/reel/rack_worker_spec.rb +67 -0
- data/spec/reel/response_spec.rb +3 -19
- data/spec/reel/server_spec.rb +28 -22
- data/spec/reel/websocket_spec.rb +92 -0
- data/spec/spec_helper.rb +24 -4
- metadata +46 -15
data/.travis.yml
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
rvm:
|
2
2
|
- 1.9.2
|
3
3
|
- 1.9.3
|
4
|
-
- ruby-head
|
4
|
+
# - ruby-head http_parser.rb not working :(
|
5
5
|
- jruby-19mode
|
6
6
|
- jruby-head
|
7
|
-
|
8
|
-
# Rubies I would like to support, but they deadlock
|
9
|
-
# - rbx-18mode
|
10
|
-
# - rbx-19mode
|
7
|
+
# - rbx-19mode choking on string encodings
|
11
8
|
|
12
9
|
notifications:
|
13
10
|
irc: "irc.freenode.org#celluloid"
|
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -3,14 +3,14 @@
|
|
3
3
|
[![Build Status](https://secure.travis-ci.org/celluloid/reel.png?branch=master)](http://travis-ci.org/celluloid/reel)
|
4
4
|
|
5
5
|
Reel is a fast, non-blocking "evented" web server built on [http_parser.rb][parser],
|
6
|
-
[Celluloid::IO][celluloidio], and [nio4r][nio4r].
|
7
|
-
|
8
|
-
|
6
|
+
[libwebsocket][websockets],[ Celluloid::IO][celluloidio], and [nio4r][nio4r]. Thanks
|
7
|
+
to Celluloid, Reel also works great for multithreaded applications and provides
|
8
|
+
traditional multithreaded blocking I/O support too.
|
9
9
|
|
10
10
|
[parser]: https://github.com/tmm1/http_parser.rb
|
11
|
+
[websockets]: https://github.com/imanel/websocket-ruby
|
11
12
|
[celluloidio]: https://github.com/celluloid/celluloid-io
|
12
13
|
[nio4r]: https://github.com/tarcieri/nio4r
|
13
|
-
[Goliath]: http://postrank-labs.github.com/goliath/
|
14
14
|
|
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
|
@@ -63,12 +63,24 @@ Reel provides an extremely simple API:
|
|
63
63
|
require 'reel'
|
64
64
|
|
65
65
|
Reel::Server.supervise("0.0.0.0", 3000) do |connection|
|
66
|
-
request = connection.request
|
67
|
-
|
68
|
-
|
66
|
+
while request = connection.request
|
67
|
+
when Reel::Request
|
68
|
+
puts "Client requested: #{request.method} #{request.url}"
|
69
|
+
connection.respond :ok, "hello, world"
|
70
|
+
when Reel::WebSocket
|
71
|
+
puts "Client made a WebSocket request to: #{request.url}"
|
72
|
+
request << "Hello there"
|
73
|
+
connection.close
|
74
|
+
break
|
75
|
+
end
|
76
|
+
end
|
69
77
|
end
|
70
78
|
```
|
71
79
|
|
80
|
+
When we read a request from the incoming connection, we'll either get back
|
81
|
+
a Reel::Request object, indicating a normal HTTP connection, or a
|
82
|
+
Reel::WebSocket object for WebSockets connections.
|
83
|
+
|
72
84
|
Status
|
73
85
|
------
|
74
86
|
|
data/bin/reel
CHANGED
@@ -2,13 +2,57 @@
|
|
2
2
|
|
3
3
|
require 'reel'
|
4
4
|
|
5
|
-
|
5
|
+
options = {}
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
parser = OptionParser.new do |opts|
|
8
|
+
opts.on "-p", "--port PORT", Integer,
|
9
|
+
"Define what port TCP port to bind to (default: 3000)" do |arg|
|
10
|
+
options[:port] = arg
|
11
|
+
end
|
12
|
+
|
13
|
+
opts.on "-a", "--address HOST",
|
14
|
+
"bind to HOST address (default: 0.0.0.0)" do |arg|
|
15
|
+
options[:host] = arg
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on "-q", "--quiet", "Quiet down the output" do
|
19
|
+
options[:quiet] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on "-e", "--environment ENVIRONMENT",
|
23
|
+
"The environment to run the Rack app on (default: development)" do |arg|
|
24
|
+
options[:environment] = arg
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on "-t", "--threads NUM", Integer,
|
28
|
+
"The number of worker threads (default: 10)" do |arg|
|
29
|
+
options[:workers] = arg
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on "-r", "--rackup FILE",
|
33
|
+
"Load Rack config from this file (default: config.ru)" do |arg|
|
34
|
+
options[:rackup] = arg
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
parser.banner = "reel <options> <rackup file>"
|
9
39
|
|
10
|
-
|
11
|
-
|
40
|
+
parser.on_tail "-h", "--help", "Show help" do
|
41
|
+
puts parser
|
42
|
+
exit 1
|
12
43
|
end
|
13
44
|
|
45
|
+
parser.parse(ARGV)
|
46
|
+
|
47
|
+
if ARGV.last =~ /\.ru$/
|
48
|
+
options[:rackup] = @argv.shift
|
49
|
+
end
|
50
|
+
|
51
|
+
handler = Rack::Handler::Reel.new(options)
|
52
|
+
|
53
|
+
Reel::Logger.info "A Reel good HTTP server!"
|
54
|
+
Reel::Logger.info "Listening on #{handler[:host]}:#{handler[:port]}"
|
55
|
+
|
56
|
+
handler.start
|
57
|
+
|
14
58
|
sleep
|
data/examples/hello_world.rb
CHANGED
@@ -6,7 +6,13 @@ addr, port = '127.0.0.1', 1234
|
|
6
6
|
|
7
7
|
puts "*** Starting server on #{addr}:#{port}"
|
8
8
|
Reel::Server.new(addr, port) do |connection|
|
9
|
-
|
10
|
-
|
9
|
+
# To use keep-alive with Reel, use a while loop that repeatedly calls
|
10
|
+
# connection.request and consumes connection objects
|
11
|
+
while request = connection.request
|
12
|
+
# Ordinarily we'd route the request here, e.g.
|
13
|
+
# route request.url
|
14
|
+
connection.respond :ok, "hello, world!"
|
15
|
+
end
|
11
16
|
|
12
|
-
|
17
|
+
# Reel takes care of closing the connection
|
18
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# FIXME: not quite complete yet, but it should give you an idea
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'reel'
|
6
|
+
|
7
|
+
class TimeServer
|
8
|
+
include Celluloid
|
9
|
+
include Celluloid::Notifications
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
run!
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
now = Time.now.to_f
|
17
|
+
sleep now.ceil - now + 0.001
|
18
|
+
|
19
|
+
every(1) { publish 'time_change', Time.now }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class TimeClient
|
24
|
+
include Celluloid
|
25
|
+
include Celluloid::Notifications
|
26
|
+
include Celluloid::Logger
|
27
|
+
|
28
|
+
def initialize(websocket)
|
29
|
+
info "Streaming time changes to client"
|
30
|
+
@socket = websocket
|
31
|
+
subscribe('time_change', :notify_time_change)
|
32
|
+
end
|
33
|
+
|
34
|
+
def notify_time_change(topic, new_time)
|
35
|
+
@socket << new_time.inspect
|
36
|
+
rescue Reel::SocketError
|
37
|
+
info "Time client disconnected"
|
38
|
+
terminate
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class WebServer < Reel::Server
|
43
|
+
include Celluloid::Logger
|
44
|
+
|
45
|
+
def initialize(host = "127.0.0.1", port = 1234)
|
46
|
+
info "Time server example starting on #{host}:#{port}"
|
47
|
+
super(host, port, &method(:on_connection))
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_connection(connection)
|
51
|
+
while request = connection.request
|
52
|
+
case request
|
53
|
+
when Reel::Request
|
54
|
+
route_request connection, request
|
55
|
+
when Reel::WebSocket
|
56
|
+
info "Received a WebSocket connection"
|
57
|
+
route_websocket request
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def route_request(connection, request)
|
63
|
+
if request.url == "/"
|
64
|
+
return render_index(connection)
|
65
|
+
end
|
66
|
+
|
67
|
+
info "404 Not Found: #{request.path}"
|
68
|
+
connection.respond :not_found, "Not found"
|
69
|
+
end
|
70
|
+
|
71
|
+
def route_websocket(socket)
|
72
|
+
if socket.url == "/timeinfo"
|
73
|
+
TimeClient.new(socket)
|
74
|
+
else
|
75
|
+
info "Received invalid WebSocket request for: #{socket.url}"
|
76
|
+
socket.close
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def render_index(connection)
|
81
|
+
info "200 OK: /"
|
82
|
+
connection.respond :ok, <<-HTML
|
83
|
+
<!doctype html>
|
84
|
+
<html lang="en">
|
85
|
+
<head>
|
86
|
+
<meta charset="utf-8">
|
87
|
+
<title>Reel WebSockets time server example</title>
|
88
|
+
<style>
|
89
|
+
body {
|
90
|
+
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
91
|
+
font-weight: 300;
|
92
|
+
text-align: center;
|
93
|
+
}
|
94
|
+
|
95
|
+
#content {
|
96
|
+
width: 800px;
|
97
|
+
margin: 0 auto;
|
98
|
+
background: #EEEEEE;
|
99
|
+
padding: 1em;
|
100
|
+
}
|
101
|
+
</style>
|
102
|
+
</head>
|
103
|
+
<script>
|
104
|
+
var SocketKlass = "MozWebSocket" in window ? MozWebSocket : WebSocket;
|
105
|
+
var ws = new SocketKlass('ws://' + window.location.host + '/timeinfo');
|
106
|
+
ws.onmessage = function(msg){
|
107
|
+
document.getElementById('current-time').innerHTML = msg.data;
|
108
|
+
}
|
109
|
+
</script>
|
110
|
+
<body>
|
111
|
+
<div id="content">
|
112
|
+
<h1>Time Server Example</h1>
|
113
|
+
<div>The time is now: <span id="current-time">...</span></div>
|
114
|
+
</div>
|
115
|
+
</body>
|
116
|
+
</html>
|
117
|
+
HTML
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
TimeServer.supervise_as :time_server
|
122
|
+
WebServer.supervise_as :reel
|
123
|
+
|
124
|
+
sleep
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'reel'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Handler
|
5
|
+
class Reel
|
6
|
+
attr_reader :options
|
7
|
+
|
8
|
+
# Don't mess with Rack::File
|
9
|
+
File = ::File
|
10
|
+
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
:host => "0.0.0.0",
|
13
|
+
:port => 3000,
|
14
|
+
:quiet => false,
|
15
|
+
:workers => 10,
|
16
|
+
:rackup => "config.ru"
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.run(app, options = {})
|
20
|
+
|
21
|
+
handler = Reel.new(options)
|
22
|
+
|
23
|
+
::Reel::Logger.info "A Reel good HTTP server!"
|
24
|
+
::Reel::Logger.info "Listening on #{handler[:host]}:#{handler[:port]}"
|
25
|
+
|
26
|
+
handler.start
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(opts = {})
|
30
|
+
opts = normalize_options(opts)
|
31
|
+
|
32
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
33
|
+
|
34
|
+
if @options[:environment]
|
35
|
+
ENV['RACK_ENV'] = @options[:environment].to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def start
|
40
|
+
Celluloid::Actor[:reel_rack_pool] = ::Reel::RackWorker.pool(size: options[:workers], args: [self])
|
41
|
+
|
42
|
+
::Reel::Server.supervise_as(:reel_server, options[:host], options[:port]) do |connection|
|
43
|
+
Celluloid::Actor[:reel_rack_pool].handle(connection.detach)
|
44
|
+
end
|
45
|
+
|
46
|
+
sleep
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](option)
|
50
|
+
@options[option]
|
51
|
+
end
|
52
|
+
|
53
|
+
def rack_app
|
54
|
+
return @options[:app] if @options[:app]
|
55
|
+
|
56
|
+
path = @options[:rackup]
|
57
|
+
|
58
|
+
unless File.exists?(path)
|
59
|
+
raise "Missing rackup file '#{path}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
@options[:app], options = Rack::Builder.parse_file path
|
63
|
+
@options.merge! options
|
64
|
+
|
65
|
+
unless @options[:quiet]
|
66
|
+
@options[:app] = Rack::CommonLogger.new(@options[:app], STDOUT)
|
67
|
+
end
|
68
|
+
|
69
|
+
@options[:app]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Transform the options that rails s reel passes
|
75
|
+
def normalize_options(options)
|
76
|
+
options.inject({}) { |h, (k,v)| h[k.downcase] = v ; h }
|
77
|
+
options[:rackup] = options[:config] if options[:config]
|
78
|
+
options
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
register :reel, Reel
|
83
|
+
end
|
84
|
+
end
|
data/lib/reel.rb
CHANGED
@@ -10,9 +10,24 @@ require 'reel/request'
|
|
10
10
|
require 'reel/request_parser'
|
11
11
|
require 'reel/response'
|
12
12
|
require 'reel/server'
|
13
|
+
require 'reel/websocket'
|
14
|
+
|
15
|
+
require 'rack'
|
16
|
+
require 'rack/handler'
|
17
|
+
require 'rack/handler/reel'
|
18
|
+
require 'reel/rack_worker'
|
13
19
|
|
14
20
|
# A Reel good HTTP server
|
15
21
|
module Reel
|
22
|
+
# Error reading a request
|
23
|
+
class RequestError < StandardError; end
|
24
|
+
|
25
|
+
# Error occured performing IO on a socket
|
26
|
+
class SocketError < RequestError; end
|
27
|
+
|
28
|
+
# Error occured during a WebSockets handshake
|
29
|
+
class HandshakeError < RequestError; end
|
30
|
+
|
16
31
|
# The method given was not understood
|
17
32
|
class UnsupportedMethodError < ArgumentError; end
|
18
33
|
end
|
data/lib/reel/connection.rb
CHANGED
@@ -3,15 +3,16 @@ module Reel
|
|
3
3
|
class Connection
|
4
4
|
class StateError < RuntimeError; end # wrong state for a given operation
|
5
5
|
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :socket, :parser
|
7
7
|
|
8
8
|
# Attempt to read this much data
|
9
9
|
BUFFER_SIZE = 4096
|
10
10
|
|
11
11
|
def initialize(socket)
|
12
|
-
@
|
12
|
+
@attached = true
|
13
|
+
@socket = socket
|
13
14
|
@keepalive = true
|
14
|
-
@parser
|
15
|
+
@parser = Request::Parser.new
|
15
16
|
reset_request
|
16
17
|
|
17
18
|
@response_state = :header
|
@@ -21,44 +22,59 @@ module Reel
|
|
21
22
|
# Is the connection still active?
|
22
23
|
def alive?; @keepalive; end
|
23
24
|
|
25
|
+
# Is the connection still attached to a Reel::Server?
|
26
|
+
def attached?; @attached; end
|
27
|
+
|
28
|
+
# Detach this connection from the Reel::Server and manage it independently
|
29
|
+
def detach
|
30
|
+
@attached = false
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
24
34
|
# Reset the current request state
|
25
|
-
def reset_request
|
26
|
-
@request_state =
|
27
|
-
@
|
35
|
+
def reset_request(state = :header)
|
36
|
+
@request_state = state
|
37
|
+
@header_buffer = "" # Buffer headers in case of an upgrade request
|
28
38
|
@parser.reset
|
29
39
|
end
|
30
40
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
begin
|
36
|
-
@parser << @socket.readpartial(BUFFER_SIZE) until @parser.headers
|
37
|
-
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
38
|
-
@keepalive = false
|
39
|
-
@socket.close unless @socket.closed?
|
40
|
-
return
|
41
|
-
end
|
42
|
-
|
43
|
-
@request_state = :body
|
41
|
+
def peer_address
|
42
|
+
@socket.peeraddr(false)
|
43
|
+
end
|
44
44
|
|
45
|
-
|
46
|
-
@
|
47
|
-
|
48
|
-
end
|
45
|
+
def local_address
|
46
|
+
@socket.addr(false)
|
47
|
+
end
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
49
|
+
# Read a request object from the connection
|
50
|
+
def request
|
51
|
+
return if @request_state == :websocket
|
52
|
+
req = Request.read(self)
|
53
|
+
|
54
|
+
case req
|
55
|
+
when Request
|
56
|
+
@request_state = :body
|
57
|
+
@keepalive = false if req['Connection'] == 'close' || req.version == "1.0"
|
58
|
+
@body_remaining = Integer(req['Content-Length']) if req['Content-Length']
|
59
|
+
when WebSocket
|
60
|
+
@request_state = @response_state = :websocket
|
61
|
+
@body_remaining = nil
|
62
|
+
@socket = nil
|
63
|
+
else raise "unexpected request type: #{req.class}"
|
54
64
|
end
|
55
65
|
|
56
|
-
|
57
|
-
|
66
|
+
req
|
67
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
68
|
+
# The client is disconnected
|
69
|
+
@request_state = :closed
|
70
|
+
@keepalive = false
|
71
|
+
nil
|
58
72
|
end
|
59
73
|
|
60
74
|
# Read a chunk from the request
|
61
75
|
def readpartial(size = BUFFER_SIZE)
|
76
|
+
raise StateError, "can't read in the `#{@request_state}' state" unless @request_state == :body
|
77
|
+
|
62
78
|
if @body_remaining and @body_remaining > 0
|
63
79
|
chunk = @parser.chunk
|
64
80
|
unless chunk
|
@@ -110,11 +126,10 @@ module Reel
|
|
110
126
|
@keepalive = false
|
111
127
|
ensure
|
112
128
|
if @keepalive
|
113
|
-
reset_request
|
114
|
-
@request_state = :header
|
129
|
+
reset_request(:header)
|
115
130
|
else
|
116
131
|
@socket.close unless @socket.closed?
|
117
|
-
|
132
|
+
reset_request(:closed)
|
118
133
|
end
|
119
134
|
end
|
120
135
|
|