WebSockets with Cramp

Rails itself is not good at asynchronous event handling. And because of this it is bad at push techniques and also not a very good candidate for real time web applications. This problem exists in Rails because we can’t keep a bidirectional socket open between our client and a Rails application.

There are many solution that solve this problem. Node.js and Socket.io are some javascript based solutions. Whereas Faye, Goliath, async_sinatra and Cramp are some Ruby based solutions.

Cramp is an asynchronous framework running inside EventMachine loop. It means that while Cramp is communicating with a client over a connection, it can handle another connection with some other client simultaneously. And all of it is done by Cramp in a very scalable fashion. You should give it a shot for your next massive multiplayer online game.

Cramp implements solutions concerning real time web applications like long polling, streaming and WebSockets. In built support for WebSockets was introduced in version 0.9.

WebSockets are W3C specifications to create a bidirectional connection between a client and a server. This specification defines a fairly decent javascript API with which we can create a WebSocket connection between a client browser and a server. Following are a few javascript functions that let us open and communicate over a WebSocket connection from our browser to a server.

if (window.WebSocket) { // Check if browser supports WebSockets.
  ws = new WebSocket("ws://...");
}

ws.onopen();  // Called when a connection is established successfully with a server.

ws.onmessage();  // Called when a server writes on the open socket.

ws.onclose(); // Called when the opened connection is terminated successfully.

Cramp provides us a Cramp::Websocket class which lets us create our own Ruby class with WebSocket support. It also lets us define some methods or actions that are reflection of callback function provided by WebSocket API.

require 'cramp'

class WebsocketEnabledClass < Cramp::Websocket

  on_start  :user_connected           # Called when a connection is established successfully with the client.
  on_finish :user_left                      # Called when a connection with the client is terminated successfully.
  on_data   :user_said_something # Called when client writes on the open socket.

  def user_connected
    ...
  end

  def user_left
    ...
  end

  def user_said_something
     ...
  end
end

There is code of a small example chat application at github that you can check out. I will explain relevant portions of code right here.

require 'cramp'
require 'erubis'
require 'usher'

Cramp::Websocket.backend = :thin

module ChatRamp
  class HomeAction < Cramp::Action
    @@template = Erubis::Eruby.new(File.read('index.erb'))

    def start
      render @@template.result(binding)
      finish
    end
  end

  class SocketAction < Cramp::Websocket
    @@users = Set.new

    on_start :user_connected
    on_finish :user_left
    on_data :message_received

    def user_connected
      @@users << self
    end

    def user_left
      @@users.delete self
    end

    def message_received(data)
      @@users.each { |u| u.render data }
    end
  end
end

routes = Usher::Interface.for(:rack) do
  add('/').to(ChatRamp::HomeAction)
  add('/socket').to(ChatRamp::SocketAction)
end

Rack::Handler::Thin.run routes, :Port => 8080

Above is our main Cramp and WebSocket enabled Ruby script. Lets decode it line by line.

require 'cramp'
require 'erubis'
require 'usher'

Cramp::Websocket.backend = :thin

On first line, we require cramp for all the WebSocket goodies. Cramp itself does not yet have any rendering engine in itself. We use erubis to render our home page once a user tries to load it in his browser. On line three we require usher. It is a simple ruby gem that is used to define routes for rack applications. Finally we tell cramp to use thin as its backend server. Rainbows is other server that is currently supported by cramp.

module ChatRamp
  class HomeAction < Cramp::Action
    @@template = Erubis::Eruby.new(File.read('index.erb'))

    def start
      render @@template.result(binding)
      finish
    end
  end
  ...
end

We create a HomeAction class within ChatRamp namespace and make it inherit from Cramp::Action. Cramp::Action is very much like ActionController::Base of Rails. start action is executed when a user goes to home page of our application. It simply renders index.erb. There is much to Cramp::Action but we wont go into it in this article.

  class SocketAction < Cramp::Websocket
    @@users = Set.new

    on_start :user_connected
    on_finish :user_left
    on_data :message_received

    def user_connected
      @@users << self
    end

    def user_left
      @@users.delete self
    end

    def message_received(data)
      @@users.each { |u| u.render data }
    end
  end

Cramp::Websocket is the class where all the Websocket action is. We define some callback methods. user_connected simply puts the WebSocket connection created between our application and browser in @@users class variable. user_left simply removes that connection whenever this connection is terminated. And message_received loops through all current connections and writes data received to them. In other words it simply broadcasts the message to all the clients.

  routes = Usher::Interface.for(:rack) do
    add('/').to(ChatRamp::HomeAction)
    add('/socket').to(ChatRamp::SocketAction)
  end

  Rack::Handler::Thin.run routes, :Port => 8080

We use Usher to define routes for our application. And finally we start thin server to listen connections on port 8080.

Following is relevant javascript client side code in index.erb.

      if (!window.WebSocket)
        alert("WebSocket not supported by this browser");

      var room = {
        join: function(name) {
          this._username = name;
          var location   = 'ws://<%= request.host_with_port %>/socket'
           // document.location.toString().replace('http:','ws:');

          this._ws           = new WebSocket(location);
          this._ws.onopen    = this._onopen;
          this._ws.onmessage = this._onmessage;
          this._ws.onclose   = this._onclose;
        },

        _onopen: function() {
          $('join').className   = 'hidden';
          $('joined').className = '';
          $('phrase').focus();
          room._send(room._username, 'has joined!');
        },

        _onmessage: function(m) {
          if (m.data) {
            var c    = m.data.indexOf(':');
            var from = m.data.substring(0, c).replace('<', '&lt;').replace('>', '&gt;');
            var text = m.data.substring(c + 1).replace('<', '&lt;').replace('>', '&gt;');

            var chat = $('chat');
            var spanFrom = document.createElement('span');
            spanFrom.className = 'from';
            spanFrom.innerHTML = from + ':&nbsp;';
            var spanText = document.createElement('span');
            spanText.className = 'text';
            spanText.innerHTML = text;
            var lineBreak = document.createElement('br');
            chat.appendChild(spanFrom);
            chat.appendChild(spanText);
            chat.appendChild(lineBreak);
            chat.scrollTop = chat.scrollHeight - chat.clientHeight;
          }
        },

        _onclose: function(m) {
          this._ws = null;
          $('join').className = '';
          $('joined').className = 'hidden';
          $('username').focus();
          $('chat').innerHTML = '';
        }

      };

Lets try to understand it line by line.

      if (!window.WebSocket)
        alert("WebSocket not supported by this browser");

Here we test if user’s browser supports WebSockets. If it does not, we simply alert the user about it.

      var room = {
        join: function(name) {
          this._username = name;
          var location   = 'ws://<%= request.host_with_port %>/socket'
           // document.location.toString().replace('http:','ws:');

          this._ws           = new WebSocket(location);
          this._ws.onopen    = this._onopen;
          this._ws.onmessage = this._onmessage;
          this._ws.onclose   = this._onclose;
        },
       ...
    }
    ...

We create a room javascript object join function is called whenever a user clicks on “join” button on home page. It uses WebSocket API to create a new WebSocket instance that is tied to /socket end point in our application. onopen is called when the connection is successfully established between browser and server after a handshake. It simply updates some HTML fragments on our home page. onclose works on same lines however it is called when connection is terminated. onmessage is called whenever server pushes something to the browser. It simply updates the home page with message pushed by the server.

Following are some pointers that might be helpful exploring Cramp and WebSockets.

Tagged as:

One thought on “WebSockets with Cramp

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>