From a9c3fd59ca298f854e55faba505d9540709aef10 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 8 Jul 2015 16:10:28 +0200 Subject: Flush out the README overview of Action Cable --- README.md | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 README.md (limited to 'README.md') diff --git a/README.md b/README.md new file mode 100644 index 0000000000..37cb1a642f --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# Action Cable -- Integrated websockets for Rails + +Action Cable seamlessly integrates websockets with the rest of your Rails application. +It allows for real-time features to be written in Ruby in the same style +and form as the rest of your Rails application, while still being performant +and scalable. It's a full-stack offering that provides both a client-side +JavaScript framework and a server-side Ruby framework. You have access to your full +domain model written with ActiveRecord or whatever. + + +## Terminology + +A single Action Cable server can handle multiple connection instances. It goes one +connection instance per websocket connection. A single user may well have multiple +websockets open to your application if they use multiple browser tabs or devices. +The client of a websocket connection is called the consumer. + +Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates +a logical unit of work, similar to what a controller does in a regular MVC setup. So +you may have a ChatChannel and a AppearancesChannel. The consumer can be subscribed to either +or to both. At the very least, a consumer should be subscribed to one channel. + +When the consumer is subscribed to a channel, they act as a subscriber. The connection between +the subscriber and the channel is, surprise-surprise, called a subscription. A consumer +can act as a subscriber to a given channel via a subscription only once. (But remember that +a physical user may have multiple consumers, one per tab/device open to your connection). + +Each channel can then again be streaming zero or more broadcastings. A broadcasting is a +pubsub link where anything transmitted by the broadcaster is sent directly to the channel +subscribers who are streaming that named broadcasting. + +As you can see, this is a fairly deep architectural stack. There's a lot of new terminology +to identify the new pieces, and on top of that, you're dealing with both client and server side +reflections of each unit. + + +## A full-stack example + +The first thing you must do is defined your ApplicationCable::Connection class in Ruby. This +is the place where you do authorization of the incoming connection, and proceed to establish it +if all is well. Here's the simplest example starting with the server-side connection class: + + # app/channels/application_cable/connection.rb + module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + protected + def find_verified_user + if current_user = User.find cookies.signed[:user_id] + current_user + else + reject_unauthorized_connection + end + end + end + end + +This relies on the fact that you will already have handled authentication of the user, and +that a successful authentication sets a signed cookie with the user_id. This cookie is then +automatically sent to the connection instance when a new connection is attempted, and you +use that to set the current_user. By identifying the connection by this same current_user, +you're also ensuring that you can later retrieve all open connections by a given user (and +potentially disconnect them all if the user is deleted or deauthorized). + +The client-side needs to setup a consumer instance of this connection. That's done like so: + + # app/assets/javascripts/cable.coffee + @App = {} + App.cable = Cable.createConsumer "http://cable.example.com" + +The http://cable.example.com address must point to your set of Action Cable servers, and it +must share a cookie namespace with the rest of the application (which may live under http://example.com). +This ensures that the signed cookie will be correctly sent. + +That's all you need to establish the connection! But of course, this isn't very useful in +itself. This just gives you the plumbing. To make stuff happen, you need content. That content +is defined by declaring channels on the server and allowing the consumer to subscribe to them. + + +## Channel example 1: User appearances + +Here's a simple example of a channel that tracks whether a user is online or not and what page they're on. +(That's useful for creating presence features like showing a green dot next to a user name if they're online). + +First you declare the server-side channel: + + # app/channels/appearance_channel.rb + class AppearanceChannel < ApplicationCable::Channel + def subscribed + current_user.appear + end + + def unsubscribed + current_user.disappear + end + + def appear(data) + current_user.appear on: data['appearing_on'] + end + + def away + current_user.away + end + end + +The #subscribed callback is invoked when, as we'll show below, a client-side subscription is initiated. In this case, +we take that opportunity to say "the current user has indeed appeared". That appear/disappear API could be backed by +Redis or a database or whatever else. Here's what the client-side of that looks like: + + # app/assets/javascripts/cable/subscriptions/appearance.coffee + App.appearance = App.cable.subscriptions.create "AppearanceChannel", + connected: -> + # Called once the subscription has been successfully completed + + appear: -> + @perform 'appear', appearing_on: @appearingOn() + + away: -> + @perform 'away' + + appearingOn: -> + $('main').data 'appearing-on' + + $(document).on 'page:change', -> + App.appearance.appear() + + $(document).on 'click', '[data-behavior~=appear_away]', -> + App.appearance.away() + false + +Simply calling App.cable.subscriptions.create will setup the subscription, which will call AppearanceChannel#subscribed, +which in turn is linked to original App.consumer -> ApplicationCable::Connection instances. + +We then link App.appearance#appear to AppearanceChannel#appear(data). This is possible because the server-side +channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these +can be reached as remote procedure calls via App.appearance#perform. + +Finally, we expose App.appearance to the machinations of the application itself by hooking the #appear call into the +Turbolinks page:change callback and allowing the user to click a data-behavior link that triggers the #away call. + + +## Channel example 2: Receiving new web notifications + +The appearance example was all about exposing server functionality to client-side invocation over the websocket connection. +But the great thing about websockets is that it's a two-way street. So now let's show an example where the server invokes +action on the client. + +This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right +streams: + + # app/channels/web_notifications.rb + class WebNotificationsChannel < ApplicationCable::Channel + def subscribed + stream_from "web_notifications_#{current_user.id}" + end + end + + # Somewhere in your app this is called, perhaps from a NewCommentJob + ActionCable.server.broadcast \ + "web_notifications_1", { title: 'New things!', body: 'All shit fit for print' } + + # Client-side which assumes you've already requested the right to send web notifications + App.cable.subscriptions.create "WebNotificationsChannel", + received: (data) -> + web_notification = new Notification data['title'], body: data['body'] + +The ActionCable.server.broadcast call places a message in the Redis' pubsub queue under the broadcasting name of "web_notifications_1". +The channel has been instructed to stream everything that arrives at "web_notifications_1" directly to the client by invoking the +#received(data) callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip +across the wire, and unpacked for the data argument arriving to #received. + + +## Dependencies + +Action Cable is currently tied to Redis through its use of the pubsub feature to route +messages back and forth over the websocket cable connection. This dependency may well +be alleviated in the future, but for the moment that's what it is. So be sure to have +Redis installed and running. + + +## Deployment + +Action Cable is powered by a combination of EventMachine and threads. The +framework plumbing needed for connection handling is handled in the +EventMachine loop, but the actual channel, user-specified, work is handled +in a normal Ruby thread. This means you can use all your regular Rails models +with no problem, as long as you haven't committed any thread-safety sins. + +But this also means that Action Cable needs to run in its own server process. +So you'll have one set of server processes for your normal web work, and another +set of server processes for the Action Cable. The former can be single-threaded, +like Unicorn, but the latter must be multi-threaded, like Puma. + + +## Alpha disclaimer + +Action Cable is currently considered alpha software. The API is almost guaranteed to change between +now and its first production release as part of Rails 5.0. Real applications using the framework +are all well underway, but as of July 8th, 2015, there are no deployments in the wild yet. + +So this current release, which resides in rails/actioncable, is primarily intended for +the adventurous kind, who do not mind reading the full source code of the framework. And it +serves as an invitation for all those crafty folks to contribute to and test what we have so far, +in advance of that general production release. + +Action Cable will move from rails/actioncable to rails/rails and become a full-fledged default +framework alongside Action Pack, Active Record, and the like once we cross the bridge from alpha +to beta software (which will happen once the API and missing pieces have solidified). + + +## Download and installation + +The latest version of Action Pack can be installed with RubyGems: + + % gem install actionpack + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/actionpack + + +## License + +Action Pack is released under the MIT license: + +* http://www.opensource.org/licenses/MIT + + +## Support + +Bug reports can be filed for the alpha development project here: + +* https://github.com/rails/actioncable/issues -- cgit v1.2.3