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 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
@@ -1,4 +1,8 @@
1
+ HEAD
2
+ ----
3
+ * Initial WebSockets support via Reel::WebSocket
4
+ * Experimental Rack adapter by Alberto Fernández-Capel
5
+
1
6
  0.1.0
2
7
  -----
3
-
4
- * First official release
8
+ * Initial release
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]. It's probably most similar to
7
- [Goliath][goliath], but thanks to Celluloid also works great for multithreaded
8
- applications and provides traditional multithreaded blocking I/O support too.
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
- puts "Client requested: #{request.method} #{request.url}"
68
- connection.respond :ok, "hello, world"
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
- host, port = "0.0.0.0", 3000
5
+ options = {}
6
6
 
7
- Reel::Logger.info "A Reel good HTTP server!"
8
- Reel::Logger.info "Listening on #{host}:#{port}"
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
- Reel::Server.supervise("0.0.0.0", 3000) do |connection|
11
- p connection
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
@@ -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
- connection.respond :ok, "hello, world!"
10
- end
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
- sleep
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
@@ -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 :request
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
- @socket = socket
12
+ @attached = true
13
+ @socket = socket
13
14
  @keepalive = true
14
- @parser = Request::Parser.new
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 = :header
27
- @request = nil
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
- # Read a request object from the connection
32
- def read_request
33
- raise StateError, "can't read header" unless @request_state == :header
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
- headers = {}
46
- @parser.headers.each do |field, value|
47
- headers[Http.canonicalize_header(field)] = value
48
- end
45
+ def local_address
46
+ @socket.addr(false)
47
+ end
49
48
 
50
- if headers['Connection']
51
- @keepalive = false if headers['Connection'] == 'close'
52
- elsif @parser.http_version == "1.0"
53
- @keepalive = false
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
- @body_remaining = Integer(headers['Content-Length']) if headers['Content-Length']
57
- @request = Request.new(@parser.http_method, @parser.url, @parser.http_version, headers, self)
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
- @request_state = :closed
132
+ reset_request(:closed)
118
133
  end
119
134
  end
120
135