Quick and Dirty introduction to ActionCable – the best WebSockets for Rails!

This post is a followup and a translation of my presentation from “The Developers Conference Florianopolis 2018

What are WebSockets good for?

  • Update the screen of many clients simultaneously when the database is updated
  • Allow many users to edit the same resource at the same time
  • Notify users that something happened

Among many other things.

I’ll not try to convince you that websockets are the best solution for these, and of course you have many options to use, for example:

  • Node.js
  • Websocket-rails
  • ActionCable

I’ll focus here in how to easily use ActionCable that is the default rails implementation and it made my life a lot easier in the last few months (I used websocket-rails before but it’s not being actively developed for a long time now…)

ActionCable basics

Besides being an awesome and simple API, ActionCable has one excellent performance (according to my tests) and has a really good connection handling.

ActionCable is a pub/sub implementation, and that makes things a lot simpler, and to simplify the pub/sub implementation it uses channels.

Each client connection connects to a channel in the server, each channel implementation, streams to a named channel defined when the client connects, allowing to use parameters to define the channel name.

Then the server can send back messages to any of the defined named channels.

Ok, writing it like that, it seems kinda complicated, but it is really simple.

For example, if you wanna send from Ruby a notification to any client, you’ll send data to one of these named channels, with a code similar to this:

ActionCable.server.broadcast 'broadcast_sample', data

where “broadcast_sample” is the name of a channel, and data is any object, for me, usually a hash with the information I want to send back to the clients.

Of course you need to define the name of the channel when the users connect, and this is done in the “ActionCable::Channel” instances in the “subscribed” method, like in the sample bellow:

class MyChannel < ApplicationCable::Channel
  def subscribed
    stream_from "broadcast_sample"
    stream_from "nome#{params[:name]}"
    stream_for current_user
  end
  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

As you can see above, from that method, it is possible to define a constant name for a topic/channel, use parameters sent by the user to define the name, and you can use the “model” variant, that is just a shortcut for creating a string name for that model.

The key is to use the “stream_from” or “stream_for” methods and use the same name later in the broadcast name.

Just to make it clearer how to send a broadcast to each of these 3 samples above, I’ll show bellow a sample code for each:

ActionCable.server.broadcast 'broadcast_sample', data

ActionCable.server.broadcast ‘nomeRodrigo’, comment: ‘Teste’, from_id: 47

ActionCable.server.broadcast_to @post, @comment

Receiving messages in Javascript

Ok, but how do you receive these messages in Javascript? it is almost as easy, just need to implement the “received” method like in the sample bellow:

App.bcsample = App.cable.subscriptions.create("BcsampleChannel", {
    connected: function () {
        // Called when the subscription is ready for use on the server
    },

    disconnected: function () {
        // Called when the subscription has been terminated by the server
    },

    received: function (data) {
        // Called when there's incoming data on the websocket for this channel
        var message = $("<div/>");
        message.text(data.message);
        $('.message-list').append(message);
    },

    speak_to_all: function (message) {
        return this.perform('speak_to_all', {user_id: window.name, message: message});
    }
});

Important points in this sample:

  • BcsampleChannel is the class name of the channel in Ruby
  • the data parameter in the received function is the data passet to the broadcast function, it should always be an object, a string does not works, I’ve tried it.

And how to call ruby code from javascript?

Just take a look at the last part of the sample above, in the “speak_to_all” method, the “perform” method, will call a  method with the same name, passing the hash parameter as the data parameter to a method “speak_to_all” in the “BcsampleChannel” class.

Of course we need to update that class to receive this call, like in the sample bellow:

class BcsampleChannel < ApplicationCable::Channel
  def subscribed
    stream_from "broadcast_sample"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak_to_all(data)
    ActionCable.server.broadcast 'broadcast_sample', data
  end
end

This sample, will receive any data and broadcast it to all connected clients.

There is one last question, how do we pass parameters to subscribed method? simple, just take a quick look at the sample bellow:

App.privatesample = App.cable.subscriptions.create({channel:"PrivatesampleChannel", windowid: window.name}, {
  connected: function() {
    // Called when the subscription is ready for use on the server
  },

  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },

  received: function(data) {
    // Called when there's incoming data on the websocket for this channel
  },
});

in the create method, instead of passing the name as a string, we need to pass an object, and the “channel” property is required, anything else will be a parameter to the channel in Ruby to use as needed.

But how about deploying?

  • You can use Redis or a database as a backend
  • If you are using passenger and nginx your are almost done!
  • Remember to setup the server path in the routes.rb
  • test and be happy

The first step is to edit the “config/cable.yml” file like the sample bellow:

production:
  adapter: redis
  url: redis://redis.example.com:6379

local: &local
  adapter: redis
  url: redis://localhost:6379

development: *local
test: *local

Then you need to add the mapping to the “config/routes.rb” file:

# Serve websocket cable requests in-process
mount ActionCable.server => '/cable'

and just add a location config to your nginx configuration like in the host bellow:

server {
    listen 80;
    server_name www.foo.com;
    root /path-to-your-app/public;
    passenger_enabled on;

    ### INSERT THIS!!! ###
    location /cable {
        passenger_app_group_name YOUR_APP_NAME_HERE_action_cable;
        passenger_force_max_concurrent_requests_per_process 0;
    }
}

Of course you have the option to start the server as a standalone server, and configure the reverse proxy, but that is a subject to another post.

You can send broadcasts to it from a sidekiq job or from rails console, as soon as you do not forget to configure the backend as shown above.

And if you have problems or questions about using or deploying ActionCable please leave a comment bellow, I’ll answer as fast as possible.