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.

@@ -0,0 +1,26 @@
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::Stream.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
+ 'Transfer-Encoding' => 'identity',
20
+ 'Content-Type' => 'text/html'
21
+ }, body]
22
+ }
23
+ end
24
+ end.to_app
25
+
26
+ Rack::Handler::Reel.run app, Port: 9292
@@ -0,0 +1,53 @@
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
+ if socket = env['rack.websocket']
17
+ socket.on_message do |m|
18
+ socket << "Server got \"#{m}\" message"
19
+ end
20
+ socket.on_error { Connections.delete socket }
21
+ Connections << socket
22
+ socket.read_every 1
23
+ end
24
+ [200, {}, []]
25
+ }
26
+ end
27
+
28
+ map '/wall' do
29
+ run lambda { |env|
30
+ msg = env['PATH_INFO'].gsub(/\/+/, '').strip
31
+ msg = Time.now if msg.empty?
32
+ Connections.each { |s| s << msg }
33
+ [200, {'Content-Type' => 'text/html'}, ["Sent \"#{msg}\" to #{Connections.size} clients"]]
34
+ }
35
+ end
36
+ end.to_app
37
+
38
+ Rack::Handler::Reel.run app, Port: 9292
39
+
40
+ __END__
41
+ <!doctype html>
42
+ <html lang="en">
43
+ <body>
44
+ <input type="button" onClick="ws.send(Math.random());" value="Send a message to server">
45
+ <div id="content"></div>
46
+ </body>
47
+ <script type="text/javascript">
48
+ ws = new WebSocket('ws://' + window.location.host + '/subscribe');
49
+ ws.onmessage = function(e) {
50
+ document.getElementById('content').innerHTML += e.data + '<br>';
51
+ }
52
+ </script>
53
+ </html>
@@ -0,0 +1,92 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'reel'
4
+
5
+ class TimeServer
6
+ include Celluloid
7
+ include Celluloid::Notifications
8
+
9
+ def initialize
10
+ run!
11
+ end
12
+
13
+ def run
14
+ now = Time.now.to_f
15
+ sleep now.ceil - now + 0.001
16
+
17
+ every(1) { publish 'time_change', Time.now }
18
+ end
19
+ end
20
+
21
+ class TimeClient
22
+ include Celluloid
23
+ include Celluloid::Notifications
24
+ include Celluloid::Logger
25
+
26
+ def initialize(websocket)
27
+ info "Streaming time changes to client"
28
+ @socket = websocket
29
+ subscribe('time_change', :notify_time_change)
30
+ end
31
+
32
+ def notify_time_change(topic, new_time)
33
+ @socket << new_time.inspect
34
+ rescue Reel::SocketError
35
+ info "Time client disconnected"
36
+ terminate
37
+ end
38
+ end
39
+
40
+ class Web
41
+ include Celluloid::Logger
42
+
43
+ def render_index
44
+ info "200 OK: /"
45
+ <<-HTML
46
+ <!doctype html>
47
+ <html lang="en">
48
+ <head>
49
+ <meta charset="utf-8">
50
+ <title>Reel WebSockets time server example</title>
51
+ <style>
52
+ body {
53
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
54
+ font-weight: 300;
55
+ text-align: center;
56
+ }
57
+
58
+ #content {
59
+ width: 800px;
60
+ margin: 0 auto;
61
+ background: #EEEEEE;
62
+ padding: 1em;
63
+ }
64
+ </style>
65
+ </head>
66
+ <script>
67
+ var SocketKlass = "MozWebSocket" in window ? MozWebSocket : WebSocket;
68
+ var ws = new SocketKlass('ws://' + window.location.host + '/timeinfo');
69
+ ws.onmessage = function(msg){
70
+ document.getElementById('current-time').innerHTML = msg.data;
71
+ }
72
+ </script>
73
+ <body>
74
+ <div id="content">
75
+ <h1>Time Server Example</h1>
76
+ <div>The time is now: <span id="current-time">...</span></div>
77
+ </div>
78
+ </body>
79
+ </html>
80
+ HTML
81
+ end
82
+ end
83
+
84
+ TimeServer.supervise_as :time_server
85
+
86
+ run Rack::URLMap.new(
87
+ "/" => Proc.new{ [200, {"Content-Type" => "text/html"}, [Web.new.render_index]]},
88
+ "/timeinfo" => Proc.new{ |env|
89
+ TimeClient.new(env["websocket.rack"])
90
+ [200, {}, []] # Fake response for middleware.
91
+ }
92
+ )
@@ -0,0 +1 @@
1
+ rackup -s reel websocket.ru -Enone
@@ -1,5 +1,3 @@
1
- # FIXME: not quite complete yet, but it should give you an idea
2
-
3
1
  require 'rubygems'
4
2
  require 'bundler/setup'
5
3
  require 'reel'
@@ -9,7 +7,7 @@ class TimeServer
9
7
  include Celluloid::Notifications
10
8
 
11
9
  def initialize
12
- run!
10
+ async.run
13
11
  end
14
12
 
15
13
  def run
@@ -18,12 +18,13 @@ module Rack
18
18
 
19
19
  def self.run(app, options = {})
20
20
 
21
- handler = Reel.new(options)
21
+ @handler = Reel.new(options.merge :app => app)
22
22
 
23
23
  ::Reel::Logger.info "A Reel good HTTP server!"
24
- ::Reel::Logger.info "Listening on #{handler[:host]}:#{handler[:port]}"
24
+ ::Reel::Logger.info "Listening on #{@handler[:host]}:#{@handler[:port]}"
25
25
 
26
- handler.start
26
+ yield @handler if block_given?
27
+ @handler.start
27
28
  end
28
29
 
29
30
  def initialize(opts = {})
@@ -73,12 +74,13 @@ module Rack
73
74
 
74
75
  # Transform the options that rails s reel passes
75
76
  def normalize_options(options)
76
- options.inject({}) { |h, (k,v)| h[k.downcase] = v ; h }
77
+ options = options.inject({}) { |h, (k,v)| h[k.downcase] = v ; h }
77
78
  options[:rackup] = options[:config] if options[:config]
79
+ options[:port] = options[:port].to_i if options[:port]
78
80
  options
79
81
  end
80
82
  end
81
83
 
82
84
  register :reel, Reel
83
85
  end
84
- end
86
+ end
data/lib/reel.rb CHANGED
@@ -1,9 +1,13 @@
1
+ require 'uri'
2
+
1
3
  require 'http/parser'
2
4
  require 'http'
3
5
  require 'celluloid/io'
4
6
 
5
7
  require 'reel/version'
6
8
 
9
+ require 'reel/mixins'
10
+
7
11
  require 'reel/connection'
8
12
  require 'reel/logger'
9
13
  require 'reel/request'
@@ -11,6 +15,7 @@ require 'reel/request_parser'
11
15
  require 'reel/response'
12
16
  require 'reel/server'
13
17
  require 'reel/websocket'
18
+ require 'reel/stream'
14
19
 
15
20
  require 'rack'
16
21
  require 'rack/handler'
@@ -19,6 +24,7 @@ require 'reel/rack_worker'
19
24
 
20
25
  # A Reel good HTTP server
21
26
  module Reel
27
+
22
28
  # Error reading a request
23
29
  class RequestError < StandardError; end
24
30
 
@@ -1,8 +1,17 @@
1
1
  module Reel
2
2
  # A connection to the HTTP server
3
3
  class Connection
4
+ include HTTPVersionsMixin
5
+ include ConnectionMixin
6
+
4
7
  class StateError < RuntimeError; end # wrong state for a given operation
5
8
 
9
+ CONNECTION = 'Connection'.freeze
10
+ TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
11
+ KEEP_ALIVE = 'Keep-Alive'.freeze
12
+ CLOSE = 'close'.freeze
13
+ CHUNKED = 'chunked'.freeze
14
+
6
15
  attr_reader :socket, :parser
7
16
 
8
17
  # Attempt to read this much data
@@ -16,7 +25,6 @@ module Reel
16
25
  reset_request
17
26
 
18
27
  @response_state = :header
19
- @body_remaining = nil
20
28
  end
21
29
 
22
30
  # Is the connection still active?
@@ -31,18 +39,6 @@ module Reel
31
39
  self
32
40
  end
33
41
 
34
- # Obtain the IP address of the remote connection
35
- def remote_ip
36
- @socket.peeraddr(false)[3]
37
- end
38
- alias_method :remote_addr, :remote_ip
39
-
40
- # Obtain the hostname of the remote connection
41
- def remote_host
42
- # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
43
- @socket.peeraddr(true)[2]
44
- end
45
-
46
42
  # Reset the current request state
47
43
  def reset_request(state = :header)
48
44
  @request_state = state
@@ -58,11 +54,9 @@ module Reel
58
54
  case req
59
55
  when Request
60
56
  @request_state = :body
61
- @keepalive = false if req['Connection'] == 'close' || req.version == "1.0"
62
- @body_remaining = Integer(req['Content-Length']) if req['Content-Length']
57
+ @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
63
58
  when WebSocket
64
59
  @request_state = @response_state = :websocket
65
- @body_remaining = nil
66
60
  @socket = nil
67
61
  else raise "unexpected request type: #{req.class}"
68
62
  end
@@ -79,19 +73,35 @@ module Reel
79
73
  def readpartial(size = BUFFER_SIZE)
80
74
  raise StateError, "can't read in the `#{@request_state}' state" unless @request_state == :body
81
75
 
82
- if @body_remaining and @body_remaining > 0
76
+ chunk = @parser.chunk
77
+ unless chunk || @parser.finished?
78
+ @parser << @socket.readpartial(size)
83
79
  chunk = @parser.chunk
84
- unless chunk
85
- @parser << @socket.readpartial(size)
86
- chunk = @parser.chunk
87
- return unless chunk
88
- end
80
+ 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
89
88
 
90
- @body_remaining -= chunk.length
91
- @body_remaining = nil if @body_remaining < 1
89
+ return '' if length == 0
92
90
 
93
- chunk
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
94
102
  end
103
+
104
+ return length && res.length == 0 ? nil : res
95
105
  end
96
106
 
97
107
  # Send a response back to the client
@@ -107,9 +117,9 @@ module Reel
107
117
  end
108
118
 
109
119
  if @keepalive
110
- headers['Connection'] = 'Keep-Alive'
120
+ headers[CONNECTION] = KEEP_ALIVE
111
121
  else
112
- headers['Connection'] = 'close'
122
+ headers[CONNECTION] = CLOSE
113
123
  end
114
124
 
115
125
  case response
@@ -122,7 +132,7 @@ module Reel
122
132
  response.render(@socket)
123
133
 
124
134
  # Enable streaming mode
125
- if response.headers['Transfer-Encoding'] == "chunked" and response.body.nil?
135
+ if response.headers[TRANSFER_ENCODING] == CHUNKED and response.body.nil?
126
136
  @response_state = :chunked_body
127
137
  end
128
138
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE
@@ -140,24 +150,23 @@ module Reel
140
150
  # Write body chunks directly to the connection
141
151
  def write(chunk)
142
152
  raise StateError, "not in chunked body mode" unless @response_state == :chunked_body
143
- chunk_header = chunk.bytesize.to_s(16) + Response::CRLF
144
- @socket << chunk_header
145
- @socket << chunk
146
- @socket << Response::CRLF
153
+ chunk_header = chunk.bytesize.to_s(16)
154
+ @socket << chunk_header + Response::CRLF
155
+ @socket << chunk + Response::CRLF
147
156
  end
148
157
  alias_method :<<, :write
149
158
 
150
159
  # Finish the response and reset the response state to header
151
160
  def finish_response
152
161
  raise StateError, "not in body state" if @response_state != :chunked_body
153
- @socket << "0" << Response::CRLF * 2
162
+ @socket << "0#{Response::CRLF * 2}"
154
163
  @response_state = :header
155
164
  end
156
165
 
157
166
  # Close the connection
158
167
  def close
159
168
  @keepalive = false
160
- @socket.close
169
+ @socket.close unless @socket.closed?
161
170
  end
162
171
  end
163
172
  end
@@ -0,0 +1,57 @@
1
+ module HTTPVersionsMixin
2
+
3
+ HTTP_VERSION_1_0 = '1.0'.freeze
4
+ HTTP_VERSION_1_1 = '1.1'.freeze
5
+ DEFAULT_HTTP_VERSION = HTTP_VERSION_1_1
6
+ end
7
+
8
+ module ConnectionMixin
9
+
10
+ # Obtain the IP address of the remote connection
11
+ def remote_ip
12
+ @socket.peeraddr(false)[3]
13
+ end
14
+ alias_method :remote_addr, :remote_ip
15
+
16
+ # Obtain the hostname of the remote connection
17
+ def remote_host
18
+ # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
19
+ @socket.peeraddr(true)[2]
20
+ end
21
+ end
22
+
23
+ module RequestMixin
24
+
25
+ def method
26
+ @http_parser.http_method
27
+ end
28
+
29
+ def headers
30
+ @http_parser.headers
31
+ end
32
+
33
+ def [] header
34
+ headers[header]
35
+ end
36
+
37
+ def version
38
+ @http_parser.http_version || HTTPVersionsMixin::DEFAULT_HTTP_VERSION
39
+ end
40
+
41
+ def url
42
+ @http_parser.url
43
+ end
44
+
45
+ def uri
46
+ @uri ||= URI(url)
47
+ end
48
+
49
+ def path
50
+ @http_parser.request_path
51
+ end
52
+
53
+ def query_string
54
+ @http_parser.query_string
55
+ end
56
+
57
+ end