A Series of Tubes, Part 2: ActionCable in Rails 23 Jun 2020
In this post we’ll dive into implementing WebSocket connections in Rails. If you’re interested in a higher level introduction to WebSockets and other communication protocols, check out part one of this series here.
Rails Generators
On of my favorite parts of Ruby on Rails is generators. Generators will automatically create files and folders which conform to Rails convention in a single line of code. In fact, rails new
is itself a generator which will create a functional, if empty, rails app from scratch. After you’ve run rails new
and decided on a database schema, you’re ready to start using generators. The syntax is pretty straightforward; here’s an example from my recent capstone project Emissary:
rails g controller User username:string email:string password:string
Generators are so powerful because they can be customized to only create the resources you need. Here I’m asking only for the controller (and by extension the model) for my User class. I’ll be writing my frontend in React, so no need to clutter my file structure with views for the User. I know from planning my project that I expect to store a username, email, and password for my users and each will be a string. One line and I’m ready to go!
Channels
Now that we have the basics of generators down, we can dive into ActionCable. ActionCable is a WebSocket implementation that was integrated into Rails 5. ActionCable works by establishing a WebSocket connection with clients, and then clients subscribe to channels to receive content. My app requires 2 connections at the moment, one for new conversations and one for messages within a given conversation. We can fire up our generator by running rails g channel message && rails g channel conversation
and delve into the structure from there.
In my app
folder I’ll now find a channels
folder as well as the traditional models
and controllers
folders. Much like controllers, channels inherit from a parent class called ApplicationChannel
. Each of my new channels contains 2 new methods subscribed
and unsubscribed
. These manage what happens when a client (henceforth called a consumer) establishes a connection with the channel. I use these methods to manage what data to send to which users. For example, here’s my MessagesChannel
subscribe method:
This method expects params from the consumer that will include a conversation id. First I search the database for a matching conversation to help filter out bogus parameters, then I establish a stream. stream_from
is a built in ActionCable method that will now broadcast any newly published messages to the consumers subscribed to this conversation.
unsubscribed
simply manages what to do when a consumer disconnects from the channel. I chose to utilize another built in method stop_all_streams
to close the channel on the server side and not broadcast needlessly.
We need a third method, however, to complete our MessagesChannel
implementation: receive
Receive
This method is where all the work happens. One of my goals for this project was to remove as many RESTful routes as possible and use WebSockets to achieve the same goals. As it turns out, channels are hooked into the models and the database just like controllers are, thanks Rails! Here’s my receive
method:
There’s a lot going on here, but at its core this should look familiar to a Rails developer. This method takes a JWT as an argument, decrypts the payload, and performs the same authorization, database reads, and commits that MessagesController
used to handle via HTTP. That payload includes all the parameters we need including: the sending user’s id, the conversation id, and the message itself. We use ActionCable to broadcast the newly received message to all users subscribed to that specific conversation. Instant messaging achieved!
This implementation works, but I want to optimize it further in the future by using another Rails component ActiveJob. ActiveJob allows me to implement a queuing system for interacting with the database and allows me to prioritize and schedule actions in the background. I believe ActiveJob will help my application scale and handle increased traffic more gracefully.
Final configuration settings
Now that we’ve set up the basics for ActionCable, we need to do some final configuration. There are two files that we need to update: /config/routes.rb
and /config/initializers/cors.rb
. Routes open up URL paths that can be hit, while CORS governs which clients can make requests to our server.
routes.rb
This one is relatively simple, we just need to add in a line to mount our ActionCable server at a given path like so:
mount ActionCable.server => '/cable'
Now we have a path available to establish a connection from our frontend.
cors.rb
DISCLAIMER: The following is meant for local development only, do not deploy your project with this CORS setup.
The cors.rb
file comes with a commented example that we need to slightly modify to get our development server open to our client.
Again, allowing any origin access to any resource is only suitable for development purposes. Once you finish building out your ActionCable project, be sure to restrict origins to your deployed client and resources available to be hit.
Stay tuned!
Hopefully you’ve enjoyed my intro to ActionCable. It’s a fascinating feature of Rails, and I’m excited to see how else I can leverage WebSocket communication. My next post will delve into React and how to implement client-side WebSocket connections.