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.
What are the benefits of using standalone server? Could you explain it in context of containerized rails app? Thank you.
Hi Terry,
if you are deploying a containerized app, a standalone server is not a good approach in my opinion, currently I deploy my rails apps as containers and using passenger is also not a good approach.
You can deploy the app to the container using a ruby server, and add a front proxy using nginx for example (this is my current setup)
And nginx will proxy both the HTTP and WS requests.