How to serve a Cramp::Websocket and a normal Rack app on the same port?

942 views Asked by At

I'm trying to use the same port to serve normal HTTP traffic as well as an HTML5 websocket via Cramp (which is built on top of EventMachine), using Ruby 1.9.3 and Thin 1.3.1. Here is a minimal, self-contained example:

require 'thin'
require 'cramp'
require 'http_router'

Cramp::Websocket.backend = :thin

class SocketApp < Cramp::Action
  self.transport = :websocket

  on_start = :connected
  on_finish = :disconnected
  on_data = :message

  def connected
    puts 'Client connected'
  end
  def disconnected
    puts 'Client disconnected'
  end
  def message(msg)
    puts "Got message: #{msg}"
    render 'Here is your reply'
  end
end

class WebApp
  def call(env)
    [ 200, { 'Content-Type' => 'text/html' }, <<EOF
<html><head>
<script>
  function init() {
    function log(msg) { document.getElementById('log').innerHTML += msg + '<br>'; }
    var socketUri = 'ws://' + document.location.host + '/socket';
    log('Socket URI: ' + socketUri);
    var socket = new WebSocket(socketUri);
    socket.onopen = function(e) {
      log('onopen');
      socket.send('Is there anybody out there?');
      log('sent message');
    };
    socket.onclose = function(e) {
      log('onclose; code = ' + e.code + ', reason = ' + e.reason);
    };
    socket.onerror = function(e) {
      log('onerror');
    };
    socket.onmessage = function(e) {
      log('onmessage; data = ' + e.data);
    };
  }
</script>
</head><body onload='init();'>
  <h1>Serving Cramp::Websocket and normal Rack app on the same port</h1>
  <p id='log'></p>
</body></html>
EOF
    ]
  end
end

app = HttpRouter.new do
  add('/socket').to SocketApp
  add('/').to WebApp.new
end

run app

If you want to try this for yourself, stick this code into a file named config.ru and run thin start. You need the gems thin, cramp and http_router to be installed.

The idea is that the JavaScript code makes a WebSocket connection to ws://localhost:3000/socket, which echoes messages sent to it, but this does not work as intended. The open event fires, there is no error upon sending the message, but we never get a response.

From the server's point of view, no connection has been made, as the Client connected message does not get printed.

Using thin start -D, I can see the HTTP 101 happening, and some binary data is being exchanged.

What am I doing wrong?

Update: If I split the file in two parts, rip out the HttpRouter, and run two thin instances on different ports, it still doesn't work. So the problem is in the socket code, not in the HttpRouter or the WebApp.

1

There are 1 answers

0
Thomas On BEST ANSWER

Well, this is cheating, but I finally solved it by switching to a different library: websocket-rack. For the curious, the corrected code follows:

require 'thin'
require 'http_router'
require 'rack/websocket'

class SocketApp < Rack::WebSocket::Application
  def on_open(env)
    puts 'Client connected'
  end
  def on_close(env)
    puts 'Client disconnected'
  end
  def on_message(env, message)
    puts "Got message: #{message}"
    send_data 'Here is your reply'
  end
end

class WebApp
  def call(env)
    [200, { 'Content-Type' => 'text/html' }, <<EOF
<html><head>
<script>
  function init() {
    function log(msg) { document.getElementById('log').innerHTML += msg + '<br>'; }
    var socketUri = 'ws://' + document.location.host + '/socket';
    log('Socket URI: ' + socketUri);
    var socket = new WebSocket(socketUri);
    socket.onopen = function(e) {
      log('onopen');
      socket.send('Is there anybody out there?');
      log('sent message');
    };
    socket.onclose = function(e) {
      log('onclose; code = ' + e.code + ', reason = ' + e.reason);
    };
    socket.onerror = function(e) {
      log('onerror');
    };
    socket.onmessage = function(e) {
      log('onmessage; data = ' + e.data);
    };
  }
</script>
</head><body onload='init();'>
  <h1>Serving WebSocket and normal Rack app on the same port</h1>
  <p id='log'></p>
</body></html>
EOF
    ]
  end
end

app = HttpRouter.new do
  add('/socket').to(SocketApp.new)
  add('/').to(WebApp.new)
end

run app