Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

pass websockets through rack environment #17

Merged
merged 10 commits into from

4 participants

@collin

Made some changes so that a WebSocket request will go through the RackWorker.

Extracted some methods for remote addr/remote host and uri parts (path/query/fragment) to modules so the could be re-used on WebSocket requests.

Right now it just sticks the WebSocket instance in the rack env at env["async.connection"] Not really ideal, but then nothing about mixing websockets and rack together is ideal.

Also on the negative side is things will be weird if some middleware depend on [status, headers, body].

Having implemented it, I'm not 100% convinced it's a worthy idea. But when hacking around with things it can be nice to pretend Rack and WebSockets can live together in harmony.

@collin

saw #16 and rebased against master

@tarcieri
Owner

Sorry I haven't commented on this yet.

So this is... interesting. It'd be really neat if there were a standard for accommodating Websockets within Rack. Attempting to create one is likewise... interesting.

Not sure how I feel about this. Perhaps I should consult some outside opinions?

@ghost

good implementation.

i have a similar one, started cause i was unattentive and missed this pull request.
so just my two cents here:

  • perhaps closing socket when app responds with a status code higher than 300?
  • is not async.connection? too EM-ish? perhaps using rack.websocket instead?
@collin

@tarcieri yeah, I hear you. If you can get others talking and opining about it please do. Getting the websocket/rack situation resolved is incredibly important.

I found the reel websocket API a breath of fresh air. But the node community is kicking ass here, not because they have the best API, but the have an API that's easy to use without the rack baggage.

@slivu I think I like both of those ideas.

@zacksiri

IMO right now websocket just seems like a pain in the ass, I've been looking into SSE lately, and it seems to be much nicer and a better fit for rack, perhaps we should look into that direction?

@ghost

@zacksiri, SSE are "supported out of the box" and has nothing to do with this pull request.

@ghost

Tony, imo it is a good start point.
I would encourage you to merge this, then we will can move forward with more Rack stuff.
Thank you.

@zacksiri

What kind of baggage does rack have? @collin

@shtirlic

+1 to this maybe some git squash for commits

@collin

@zacksiri you have to return the [status,headers,body] tuple even though http allows for streamed responses. Any API to provide a socket-like object in rack has to hack around that basic rack assumption.

@tarcieri @slivu you guys think I should squash this down?

@tarcieri
Owner

@collin honestly I don't care. For a project like this I'd probably prefer not to squash

@collin

@tarcieri :) I will leave it alone.

@ghost

@collin, the important for me is to have it in master, no matter how :)

@tarcieri
Owner

@collin @silvu Okay, how about this: I'm a fan of rack.websocket over async.connection. How about change that and we'll call it good? ;)

lib/reel/rack_worker.rb
((18 lines not shown))
end
end
end
+ def handle_request(request, connection)
+ env = rack_env(request, connection)
+ status, headers, body_parts = @handler.rack_app.call(env)

can we call just @app.call(env) here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/reel/rack_worker.rb
((18 lines not shown))
end
end
end
+ def handle_request(request, connection)
+ env = rack_env(request, connection)
+ status, headers, body_parts = @handler.rack_app.call(env)
+ body = response_body(body_parts)
+
+ connection.respond Response.new(status, headers, body)
+ ensure
+ body.close if body.respond_to?(:close)
+ body_parts.close if body_parts.respond_to?(:close)
+ end
+
+ def handle_websocket(request, connection)
+ env = rack_env(request, connection)
+ @handler.rack_app.call(env)

the same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ghost

@tarcieri,

"change that and we'll call it good? ;)"

by calling it good do you mean merge it? :)

@tarcieri
Owner

@silvu confirm

@ghost

@tarcieri, looks ok to me.

async stuff converted to rack.websocket, merged well with @shtirlic's updates and all specs passing.

please merge it to stop bothering you about this :)

thank you.

@tarcieri
Owner

Cool, let's give this a shot

@tarcieri tarcieri merged commit 667a233 into celluloid:master
@tarcieri tarcieri referenced this pull request in rack/rack
Closed

Hijack #481

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
92 examples/websocket.ru
@@ -0,0 +1,92 @@
+require 'rubygems'
+require 'bundler/setup'
+require 'reel'
+
+class TimeServer
+ include Celluloid
+ include Celluloid::Notifications
+
+ def initialize
+ run!
+ end
+
+ def run
+ now = Time.now.to_f
+ sleep now.ceil - now + 0.001
+
+ every(1) { publish 'time_change', Time.now }
+ end
+end
+
+class TimeClient
+ include Celluloid
+ include Celluloid::Notifications
+ include Celluloid::Logger
+
+ def initialize(websocket)
+ info "Streaming time changes to client"
+ @socket = websocket
+ subscribe('time_change', :notify_time_change)
+ end
+
+ def notify_time_change(topic, new_time)
+ @socket << new_time.inspect
+ rescue Reel::SocketError
+ info "Time client disconnected"
+ terminate
+ end
+end
+
+class Web
+ include Celluloid::Logger
+
+ def render_index
+ info "200 OK: /"
+ <<-HTML
+ <!doctype html>
+ <html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Reel WebSockets time server example</title>
+ <style>
+ body {
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+ font-weight: 300;
+ text-align: center;
+ }
+
+ #content {
+ width: 800px;
+ margin: 0 auto;
+ background: #EEEEEE;
+ padding: 1em;
+ }
+ </style>
+ </head>
+ <script>
+ var SocketKlass = "MozWebSocket" in window ? MozWebSocket : WebSocket;
+ var ws = new SocketKlass('ws://' + window.location.host + '/timeinfo');
+ ws.onmessage = function(msg){
+ document.getElementById('current-time').innerHTML = msg.data;
+ }
+ </script>
+ <body>
+ <div id="content">
+ <h1>Time Server Example</h1>
+ <div>The time is now: <span id="current-time">...</span></div>
+ </div>
+ </body>
+ </html>
+ HTML
+ end
+end
+
+TimeServer.supervise_as :time_server
+
+run Rack::URLMap.new(
+ "/" => Proc.new{ [200, {"Content-Type" => "text/html"}, [Web.new.render_index]]},
+ "/timeinfo" => Proc.new{ |env|
+ TimeClient.new(env["websocket.rack"])
+ [200, {}, []] # Fake response for middleware.
+ }
+)
View
1  examples/websocket_rack.sh
@@ -0,0 +1 @@
+rackup -s reel websocket.ru -Enone
View
3  lib/reel.rb
@@ -4,6 +4,9 @@
require 'reel/version'
+require 'reel/remote_connection'
+require 'reel/uri_parts'
+
require 'reel/connection'
require 'reel/logger'
require 'reel/request'
View
14 lib/reel/connection.rb
@@ -1,6 +1,8 @@
module Reel
# A connection to the HTTP server
class Connection
+ include RemoteConnection
+
class StateError < RuntimeError; end # wrong state for a given operation
CONNECTION = 'Connection'.freeze
@@ -39,18 +41,6 @@ def detach
self
end
- # Obtain the IP address of the remote connection
- def remote_ip
- @socket.peeraddr(false)[3]
- end
- alias_method :remote_addr, :remote_ip
-
- # Obtain the hostname of the remote connection
- def remote_host
- # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
- @socket.peeraddr(true)[2]
- end
-
# Reset the current request state
def reset_request(state = :header)
@request_state = state
View
44 lib/reel/rack_worker.rb
@@ -1,6 +1,7 @@
module Reel
class RackWorker
include Celluloid
+ include Celluloid::Logger
INITIAL_BODY = ''
@@ -41,6 +42,7 @@ class RackWorker
RACK_URL_SCHEME = 'rack.url_scheme'.freeze
ASYNC_CALLBACK = 'async.callback'.freeze
ASYNC_CLOSE = 'async.close'.freeze
+ RACK_WEBSOCKET = 'rack.websocket'.freeze
PROTO_RACK_ENV = {
RACK_VERSION => Rack::VERSION,
@@ -61,19 +63,31 @@ def initialize(handler)
def handle(connection)
while request = connection.request
- begin
- env = rack_env(request, connection)
- status, headers, body_parts = @app.call(env)
- body = response_body(body_parts)
-
- connection.respond Response.new(status, headers, body)
- ensure
- body.close if body.respond_to?(:close)
- body_parts.close if body_parts.respond_to?(:close)
+ case request
+ when Request
+ handle_request(request, connection)
+ when WebSocket
+ handle_websocket(request, connection)
end
end
end
+ def handle_request(request, connection)
+ env = rack_env(request, connection)
+ status, headers, body_parts = @app.call(env)
+ body = response_body(body_parts)
+
+ connection.respond Response.new(status, headers, body)
+ ensure
+ body.close if body.respond_to?(:close)
+ body_parts.close if body_parts.respond_to?(:close)
+ end
+
+ def handle_websocket(request, connection)
+ env = rack_env(request, connection)
+ @app.call(env)
+ end
+
def response_body(body_parts)
if body_parts.respond_to?(:to_path)
File.new(body_parts.to_path)
@@ -90,8 +104,16 @@ def rack_env(request, connection)
env[SERVER_NAME] = request[HOST].to_s.split(':').first || @handler[:Host]
env[SERVER_PORT] = @handler[:port].to_s
- env[REMOTE_ADDR] = connection.remote_ip
- env[REMOTE_HOST] = connection.remote_host
+ case request
+ when WebSocket
+ remote_connection = request
+ env[RACK_WEBSOCKET] = request
+ when Request
+ remote_connection = connection
+ end
+
+ env[REMOTE_ADDR] = remote_connection.remote_ip
+ env[REMOTE_HOST] = remote_connection.remote_host
env[PATH_INFO] = request.path
env[REQUEST_METHOD] = request.method.to_s.upcase
View
13 lib/reel/remote_connection.rb
@@ -0,0 +1,13 @@
+module RemoteConnection
+ # Obtain the IP address of the remote connection
+ def remote_ip
+ @socket.peeraddr(false)[3]
+ end
+ alias_method :remote_addr, :remote_ip
+
+ # Obtain the hostname of the remote connection
+ def remote_host
+ # NOTE: Celluloid::IO does not yet support non-blocking reverse DNS
+ @socket.peeraddr(true)[2]
+ end
+end
View
20 lib/reel/request.rb
@@ -2,6 +2,8 @@
module Reel
class Request
+ include URIParts
+
attr_accessor :method, :version, :url, :headers
UPGRADE = 'Upgrade'.freeze
@@ -22,7 +24,7 @@ def self.read(connection)
upgrade = headers[UPGRADE]
if upgrade && upgrade.downcase == WEBSOCKET
- WebSocket.new(connection.socket, parser.url, headers)
+ WebSocket.new(connection.socket, parser.http_method, parser.url, headers)
else
Request.new(parser.http_method, parser.url, parser.http_version, headers, connection)
end
@@ -39,22 +41,6 @@ def [](header)
@headers[header]
end
- def uri
- @uri ||= URI(url)
- end
-
- def path
- uri.path
- end
-
- def query_string
- uri.query
- end
-
- def fragment
- uri.fragment
- end
-
def body
@body ||= begin
raise "no connection given" unless @connection
View
17 lib/reel/uri_parts.rb
@@ -0,0 +1,17 @@
+module URIParts
+ def uri
+ @uri ||= URI(url)
+ end
+
+ def path
+ uri.path
+ end
+
+ def query_string
+ uri.query
+ end
+
+ def fragment
+ uri.fragment
+ end
+end
View
13 lib/reel/websocket.rb
@@ -2,10 +2,13 @@
module Reel
class WebSocket
- attr_reader :url, :headers
+ include RemoteConnection
+ include URIParts
- def initialize(socket, url, headers)
- @socket, @url, @headers = socket, url, headers
+ attr_reader :url, :headers, :method
+
+ def initialize(socket, method, url, headers)
+ @socket, @method, @url, @headers = socket, method, url, headers
handshake = ::WebSocket::ClientHandshake.new(:get, url, headers)
@@ -45,6 +48,10 @@ def read
msg
end
+ def body
+ nil
+ end
+
def write(msg)
@socket << ::WebSocket::Message.new(msg).to_data
msg
View
15 spec/reel/rack_worker_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Reel::RackWorker do
-
let(:endpoint) { URI(example_url) }
let(:worker) do
@@ -45,6 +44,20 @@
end
end
+ context "WebSocket" do
+ include WebSocketHelpers
+
+ it "places websocket into rack env" do
+ with_socket_pair do |client, connection|
+ client << handshake.to_data
+ request = connection.request
+ env = worker.rack_env(request, connection)
+
+ env["rack.websocket"].should == request
+ end
+ end
+ end
+
it "delegates web requests to the rack app" do
ex = nil
View
19 spec/reel/websocket_spec.rb
@@ -1,26 +1,11 @@
require 'spec_helper'
describe Reel::WebSocket do
- let(:example_host) { "www.example.com" }
- let(:example_path) { "/example"}
- let(:example_url) { "ws://#{example_host}#{example_path}" }
+ include WebSocketHelpers
+
let(:example_message) { "Hello, World!" }
let(:another_message) { "What's going on?" }
- let :handshake_headers do
- {
- "Host" => example_host,
- "Upgrade" => "websocket",
- "Connection" => "Upgrade",
- "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==",
- "Origin" => "http://example.com",
- "Sec-WebSocket-Protocol" => "chat, superchat",
- "Sec-WebSocket-Version" => "13"
- }
- end
-
- let(:handshake) { WebSocket::ClientHandshake.new(:get, example_url, handshake_headers) }
-
it "performs websocket handshakes" do
with_socket_pair do |client, connection|
client << handshake.to_data
View
26 spec/spec_helper.rb
@@ -25,7 +25,8 @@ def with_socket_pair
peer = server.accept
begin
- yield client, Reel::Connection.new(peer)
+ connection = Reel::Connection.new(peer)
+ yield client, connection
ensure
server.close rescue nil
client.close rescue nil
@@ -65,3 +66,26 @@ def to_s
(@body ? @body : '')
end
end
+
+module WebSocketHelpers
+ def self.included(spec)
+ spec.instance_eval do
+ let(:example_host) { "www.example.com" }
+ let(:example_path) { "/example"}
+ let(:example_url) { "ws://#{example_host}#{example_path}" }
+ let :handshake_headers do
+ {
+ "Host" => example_host,
+ "Upgrade" => "websocket",
+ "Connection" => "Upgrade",
+ "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==",
+ "Origin" => "http://example.com",
+ "Sec-WebSocket-Protocol" => "chat, superchat",
+ "Sec-WebSocket-Version" => "13"
+ }
+ end
+
+ let(:handshake) { WebSocket::ClientHandshake.new(:get, example_url, handshake_headers) }
+ end
+ end
+end
Something went wrong with that request. Please try again.