Action Cable is a framework for integrating WebSockets with a Rails application. It allows you to include real-time features in your application, like live chat or push notifications.
WebSockets is a communication protocol between clients and servers that is an alternative to HTTP. Like HTTP, WebSockets is built on the TCP protocol. Unlike HTTP, though, WebSockets is fully 2-way, or duplex:
- With HTTP, the only way for a server to send data to a client is by the client's initiating a request to the server.
- With WebSockets, a server can send data to a client without an explicit request from the client.
In the context of live chat, for example, this means that new messages can be pushed to the client without the client having to ask the server, 'Any new messages?'.
Unlike the stateless request-response cycle of HTTP, a client using WebSockets will establish a persistent connection with a server, usually upon loading the web app.
As long as this connection is open, the client and server can freely send data to each other. The connection stays open until either the server or the client explicitly closes it, although many WebSocket frameworks will automatically take care of closing and/or re-opening connections for you when you close tabs, experience network interruptions, etc.
Because connections are persistent, and thus stateful, servers need to store
data about connections somewhere. An on-disk database such as Postgres would
work; however, it would be quite slow. For this reason, many apps use the
Redis
library to manage WebSocket connections. Redis
keeps an in-memory
key-value data store, which is much faster than a traditional database. It's the
default option for the Rails production environment.
Many WebSocket frameworks, including Action Cable, build a Pub/Sub (Publish/Subscribe) model on top of this basic connection infrastructure. This is a way of organizing what data the server should send to which connected clients.
Imagine the simplest possible WebSocket system: a news app that, whenever a new story comes in, broadcasts a notification to every connected client. It's easy: every connected client receives every broadcast. But this system wouldn't work for a chat room app. When a new message gets posted, only the users in that same room should receive a broadcast containing the new message.
In a Pub/Sub model, clients can subscribe to a particular subset of all the broadcasts the publisher (server) sends out; in Action Cable, each subset is called a stream.
The way it works: every broadcast belongs to a particular stream identified by a
unique string — say, chat_room_12
. When a client subscribes to a stream, this
means they want to receive every broadcast belonging to that stream. Then, for
every broadcast, the server checks to see which clients have subscribed to the
broadcast's associated stream, and sends it only to those clients.
Most WebSocket frameworks will add on more layers of abstraction to this
Pub/Sub model. In Action Cable, channels
are used to organize streams. Let's
say that in addition to a chat_room_12
stream, there are chat_room_14
and
chat_room_312
streams. Clearly, all of these streams are alike; with Action
Cable, they'd be managed by the same channel.
Channels are roughly analogous to controllers: just like HTTP requests related
to users might go through the UsersController
, subscriptions to chat room
streams might go through the ChatRoomsChannel
.
Action Cable, like most WebSocket frameworks, features both a client-side
library and a server-side library. Both are included when you start a new Rails
project: the server-side library is built into Rails proper, and the client-side
JavaScript library, @rails/actioncable
, is included in the default Rails
package.json
.FIX
Note: if you created your Rails project with the --skip-javascript
flag,
you'll need to npm install @rails/actioncable
yourself.
Your backend Action Cable files live in app/channels. It starts with two files, connection.rb and channel.rb, in a subdirectory application_cable:
- connection.rb defines
ApplicationCable::Connection
. A single instance of this class is created for each client that opens a WebSocket connection with the server.- Usually, you'll add methods here, such as
current_user
, which identify / authenticate the user who created the connection. - You can't use
ApplicationController#current_user
, unfortunately, as your router doesn't create a controller instance when a client opens a WebSocket connection. - Note that a client can create multiple channel/stream subscriptions, all of which use the same underlying connection.
- Usually, you'll add methods here, such as
- channel.rb defines
ApplicationCable::Channel
, which is the parent class of each channel you define. Each instance of a channel represents a single subscription, which is associated with one (or more) streams.
Inside the app/channels folder, you'll create a new file,
_channel.rb, for each channel you want to create - roughly one
for each real-time feature your app includes. There, you'll define a class that
inherits from ApplicationCable::Channel
. For example, chat_channel.rb
might look something like this:
class ChatChannel < ApplicationCable::Channel
def subscribed
# stream_from "fun_stream_name"
stream_for Room.find_by(id: params[:id])
end
end
Inside each channel you'll generally define a subscribed
method, which gets
called after a client subscribes to the channel. In this method, you'll set up a
subscription to a particular stream, often using data passed in with the
subscription request, which is available in params
.
You can supply the stream's name directly to stream_from
or, by passing an
Active Record model to stream_for
, you can let Rails determine the stream
name based on the combination of the current channel and the supplied model.
To send out a broadcast, which you can do from anywhere in your app, such as in your controller actions, you have two options:
-
If you used
stream_from
:ActionCable.server.broadcast(stream_name, broadcast_data)
-
If you used
stream_for
:SomeChannel.broadcast_to(model, broadcast_data)
Since streams are often associated with particular models, stream_for
is more
commonly used. It's also easier--you don't have to enforce a consistent naming
convention for streams--and less likely to silently fail, since there's no risk
of misspelling a stream name. For these reasons, stream_for
should generally
be your go-to option.
In each case, broadcast_data
can be any serializable object: a string, an
array, a hash, a model (which gets converted to a hash), etc.
A new Rails 6 project will by default include a file
app/javascript/channels/consumer.js
, with the following code in it:
import { createConsumer } from "@rails/actioncable"
export default createConsumer()
This exports a consumer
object, which represents the client's WebSocket
connection to the server. You'll import this consumer
object in your component
files in order to subscribe to channels, with consumer.subscriptions.create()
:
consumer.subscriptions.create(
{ channel: 'NameOfChannel', /* other data needed to set up subscription */ },
{ received: broadcast => {} }
);
The first argument is an object that specifies what you want to subscribe to.
The channel
property is mandatory; any other data is passed along as params
to the backend channel.
The second argument should be an object with a received
function, which will
get invoked whenever a broadcast comes in from the server, with broadcasted data
passed in as an argument.
A subscription
object is returned, which you can use to unsubscribe by
calling subscription.unsubscribe()
.
Note: If you used the --skip-javascript
flag when creating your Rails
project, you'll need to create the consumer.js
file yourself. You can do so at
the top level of your frontend
folder.
As mentioned earlier, an app using Action Cable needs some system for managing and keeping track of active connections and subscriptions; this system is called an 'adapter'.
You can configure adapters for different environments in the config/cable.yml
file. The default configuration, which you won't need to change, should look
something like this:
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: action_cable_demo_production
The development and production environments use async
and test
adapters,
respectively, which both store connection data in-memory in your Rails server.
The production environment uses the redis
adapter. This adapter requires the
URL of a Redis server, which by default is retrieved from the REDIS_URL
environment variable.
To get Action Cable working in your production environment using Redis, you need to:
- Install the
redis
gem, by uncommenting it in your Gemfile and runningbundle install
. - Set up a Redis server, then set the
REDIS_URL
environment variable in your production environment to point to this server's URL.
Heroku makes the second step easy. Just go to the Heroku dashboard of your app,
then go to the Resources
tab. In the Add-ons
section, search for and add
Heroku Redis
. You'll need to submit an order form, but don't worry, the
default plan is free. Heroku will automatically set up the Redis server and set
the REDIS_URL
environment variable for you.
That's it! Now, Action Cable should work in production.
Note: if you are making a web service and you would like to allow clients to connect to Action Cable from other websites or domains, you'll need to explicitly allow this. Learn how to do this here.