WebSockets on Rails 4 and Ruby 2

WebSockets are an exciting new HTML5 technology which has finally begun to pick up enough browser, server and library support to see much wider adoption, potentially driving a move towards a signficantly new kind/kinds of web client/server communication.

We've been making use of websockets with socket.io and node.js, but good old Rails doesn't need to be left out. We're going to use Dan Knox's slick high-level websocket-rails gem, and actually copy/port over a good bit of code from the slightly outdated example project.

But why stop with WebSockets? It's 2013. Let's use Ruby 2 and Rails 4, too.

That even means we can skip the step of removing Rack::Lock, because Rails 4 is threadsafe by default. (N.B: only in production, see the comment by Noah Gibbs below)

Setup

So first we'll install Ruby 2, Rails 4, and setup a new project (this assumes you're using rbenv, YMMV):

rbenv install 2.0.0-p247
export RBENV_VERSION=2.0.0-p247
gem install rails --version 4.0.0 --no-ri --no-rdoc
rails new ws42

Then just add to the Gemfile:


ruby '2.0.0'
gem 'websocket-rails'

And install:

bundle install
rails g websocket_rails:install

Code

Now we're ready to go with WebSockets on Rails. Our app is going to be incredibly simple: just a single normal Rails controller + view, which will provide the client UI for the chat, and a WebSocketRails event router and controller for handling incoming websocket frames.

From the view we're just going to create a div and set the websocket URL (sans-scheme) with an HTML5 data attribute

<div id="chat" class="well" 
      data-uri="<%= request.host %>:<%= request.port %>/websocket">
</div>

Then that URL is used by our "unobtrusive" client-side CoffeeScript to wire-up the chat div so events are sent between the DOM and our open websocket connection, after pulling in the URL with jQuery's .data().

The core of which looks like this:


jQuery ->
  window.chatController = new Chat.Controller($('#chat').data('uri'), true);

class Chat.Controller
  constructor: (url,useWebSockets) ->
    @messageQueue = []
    @dispatcher = new WebSocketRails(url,useWebSockets)
    @dispatcher.on_open = @createGuestUser
    @bindEvents()

  bindEvents: =>
    @dispatcher.bind 'new_message', @newMessage
    @dispatcher.bind 'user_list', @updateUserList
    $('input#user_name').on 'keyup', @updateUserInfo
    $('#send').on 'click', @sendMessage
    $('#message').keypress (e) -> $('#send').click() if e.keyCode == 13

  newMessage: (message) =>
    @messageQueue.push message
    @shiftMessageQueue() if @messageQueue.length > 15
    @appendMessage message

  sendMessage: (event) =>
    event.preventDefault()
    message = $('#message').val()
    @dispatcher.trigger 'new_message', {user_name: @user.user_name, msg_body: message}
    $('#message').val('')

Then in a Rails initializer we use WebsocketRails to configure an EventMap.

Similar to the Rails routes.rb config(but with some fancy event/stream-inspired features), this code maps incoming websocket frames to methods in WebsocketRails controllers:

WebsocketRails::EventMap.describe do
  subscribe :client_connected,    to: ChatController, with_method: :client_connected
  subscribe :new_message,         to: ChatController, with_method: :new_message
  subscribe :new_user,            to: ChatController, with_method: :new_user
  subscribe :change_username,     to: ChatController, with_method: :change_username
  subscribe :client_disconnected, to: ChatController, with_method: :delete_user
end
And finally our sole controller, ChatController, looks about like this:
class ChatController < WebsocketRails::BaseController
  def client_connected
    system_msg :new_message, "client #{client_id} connected"
  end

  def new_message
    user_msg :new_message, message[:msg_body].dup
  end

  def new_user
    connection_store[:user] = { user_name: sanitize(message[:user_name]) }
    broadcast_user_list
  end

  def change_username
    connection_store[:user] = sanitize(message)
    broadcast_user_list
  end

  def delete_user
    connection_store[:user] = nil
    system_msg "client #{client_id} disconnected"
    broadcast_user_list
  end

  def broadcast_user_list
    users = connection_store.collect_all(:user)
    broadcast_message :user_list, users
  end
end

Demo

And we're about ready to go - we just go ahead and deploy our new app to Pogoapp as a normal ruby app:

git init && git add -A && git commit -m "initial"
pogo create ws42 --remote pogoapp
git push pogoapp

You can fork the app on Pogoapp, try out the live demo or browse the source on github.

If you want to peek under the hood, Chrome is great for watching WebSocket traffic, frame-by-frame. You can try that out with the chat.

Be sure to try changing your username - as a demonstration it updates live both on your client and in the user list of all the other clients.

Update: Nick Gauthier wrote a new library called Tubesock for handling WebSockets with Rails 4 and Rack Hijack that may have some efficiency gains over the underlying Faye/EventMachine implementation used by websocket-rails. He's blogged about that here - it's well worth a read for interested rubyists. We've booted his tubesock-example chat app on Pogoapp too (fork a copy)

If you're interested in websocket-enabled Rails PaaS hosting, Pogoapp does that.