Обзор Action Cable
В этом руководстве вы изучите, как работает Action Cable, и как использовать WebSockets для внедрения функционала реального времени в ваше приложение Rails.
После прочтения этого руководства, вы узнаете:
- Что такое Action Cable и об его интеграции на бэкенде и фронтенде
- Как настроить Action Cable
- Как настроить каналы
- О настройке развертывания и архитектуры для запуска Action Cable
Введение
Action Cable с легкостью интегрирует WebSockets с остальными частями приложения Rails. Он позволяет писать функционал реального времени на Ruby в стиле и формате остальной части приложения Rails, в то же время являясь производительным и масштабируемым. Он представляет полный стек, включая клиентский фреймворк на JavaScript и серверный фреймворк на Ruby. Вы получаете доступ к моделям, написанным с помощью Active Record или другой ORM.
Что такое Pub/Sub
Pub/Sub), или Publish-Subscribe, относится к парадигме очереди сообщений, когда отправители информации (publishers) посылают данные в абстрактный класс получателей (subscribers), без указания отдельных получателей. Action Cable использует этот подход для коммуникации между сервером и множеством клиентов.
Серверные компоненты
Соединения
Соединения (connection) формируют основу взаимоотношения клиента с сервером. Для каждого WebSocket, принимаемого сервером, на стороне сервера будет инициализирован объект соединения. Этот объект становится родителем для всех подписок на канал, которые создаются впоследствии. Само соединение не работает с какой-либо определенной логикой приложения после аутентификации и авторизации. Клиент соединения WebSocket называется потребителем соединения (consumer). Отдельный пользователь создаст одну пару потребитель-соединение на каждую вкладку браузера, окно или устройство, которые он использует.
Соединения - это экземпляры класса ApplicationCable::Connection
. В этом классе вы авторизуете входящее соединение и приступаете к его созданию, если пользователь может быть идентифицирован.
(connection-setup) Настройка соединения
# 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
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Здесь identified_by
— это идентификатор соединения, который может быть использован, чтобы найти определенное соединение позже. Отметьте, что все, помеченное как идентификатор, автоматически создаст делегирование с тем же именем в каждом экземпляре канала, унаследованного от соединения.
Этот пример полагается на факт, что вы уже провели аутентификацию пользователя где-то в вашем приложении, и что успешная аутентификация устанавливает подписанные куки с ID пользователя.
Тогда куки автоматически посылаются в экземпляр соединения при попытке нового соединения, и используются для установления current_user
. Идентифицировав соединения тем же текущим пользователем, вы также удостоверяетесь, что в дальнейшем можете получить все открытые соединения данного пользователя (и потенциально рассоединить их все, если пользователь удален или не авторизован).
Каналы
Канал инкапсулирует логическую единицу работы, схожей с той, что делает контроллер в обычном MVC. По умолчанию Rails создает родительский класс ApplicationCable::Channel
для инкапсуляции логики, общей для ваших каналов.
(parent-channel-setup) Настройка родительского канала
# app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
Далее можно создать собственные классы каналов. Например, можно создать ChatChannel
и AppearanceChannel
:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end
Затем потребитель может быть подписан на один или оба этих канала.
Подписки
Потребитель подписывается на канал, действуя как подписчик (subscriber). Это соединение называется подпиской. Созданные сообщения затем маршрутизируются на эти подписки на канал, основываясь на идентификаторе, посланным потребителем cable.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
# Вызывается, когда потребитель успешно
# стал подписчиком этого канала
def subscribed
end
end
Клиентские компоненты
Соединения
Потребителям нужен экземпляр соединения на их стороне. Оно может быть установлено с использованием следующего JavaScript, который генерируется в Rails по умолчанию:
(connect-consumer) Присоединение потребителя
// app/assets/javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
Это подготовит потребителя, который по умолчанию присоединится к /cable
на вашем сервере. Соединение не будет установлено, пока вы не определите хотя бы одну подписку, в которой вы заинтересованы.
(Subscriber) Подписчик
Потребитель становится подписчиком создав подписку на заданный канал:
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }
# app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create { channel: "AppearanceChannel" }
Хотя это создает подписку, функционал требует отклика на полученные данные, что будет описано позже.
Потребитель может действовать как подписчик на заданный канал любое количество раз. Например, потребитель может подписаться на несколько комнат чата одновременно:
App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
Клиент-серверное взаимодействие
Потоки
Потоки предоставляют механизм, с помощью которого каналы направляют опубликованный контент (трансляции) их подписчикам.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
Если у вас есть поток, относящийся к модели, тогда используемая трансляция может быть сгенерирована из модели и канала. Следующий пример подпишет на трансляцию вида comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE
class CommentsChannel < ApplicationCable::Channel
def subscribed
post = Post.find(params[:id])
stream_for post
end
end
Затем можно транслировать на этот канал следующим образом:
CommentsChannel.broadcast_to(@post, @comment)
(broadcasting) Трансляция
Трансляция — это ссылка pub/sub, по которой все, переданное издателем (publisher), направляется непосредственно подписчикам канала, которые читают из потока трансляции с этим именем. Каждый канал может писать в поток ноль или более трансляций. Трансляции — это очередь реального времени. Если потребитель не читает поток (не подписан на данный канал), он не получит трансляцию, когда присоединится позже.
Трансляции вызываются где угодно в приложении Rails:
WebNotificationsChannel.broadcast_to(
current_user,
title: 'New things!',
body: 'All the news fit to print'
)
Вызов WebNotificationsChannel.broadcast_to
помещает сообщение в очередь pubsub текущего адаптера подписки (по умолчанию redis
для production и async
для development и test сред) под отдельным именем трансляции для каждого пользователя. Для пользователя с ID 1, имя трансляции будет web_notifications:1
.
Канал проинструктирован писать в поток все, что приходит в web_notifications:1
, непосредственно на клиент, вызывая колбэк received
.
Подписки
Когда потребитель подписывается на канал, он действует как подписчик. Это соединение называется подпиской. Затем, входящие сообщения направляются на эти подписки на канал, основываясь на идентификаторе, посланным потребителем cable.
# app/assets/javascripts/cable/subscriptions/chat.coffee
# Предполагаем, что вы уже запросили право посылать веб-уведомления
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
received: (data) ->
@appendLine(data)
appendLine: (data) ->
html = @createLine(data)
$("[data-chat-room='Best Room']").append(html)
createLine: (data) ->
"""
<article class="chat-line">
<span class="speaker">#{data["sent_by"]}</span>
<span class="body">#{data["body"]}</span>
</article>
"""
Передача параметров в каналы
Вы можете передавать параметры из клиента на сервер при создании подписки. Например:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
Объект, переданный в качестве первого аргумента в subscriptions.create
, станет хэшем params в канале cable. Ключевое слово channel
обязательное:
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
received: (data) ->
@appendLine(data)
appendLine: (data) ->
html = @createLine(data)
$("[data-chat-room='Best Room']").append(html)
createLine: (data) ->
"""
<article class="chat-line">
<span class="speaker">#{data["sent_by"]}</span>
<span class="body">#{data["body"]}</span>
</article>
"""
# Это вызывается где-нибудь в приложении,
# возможно из NewCommentJob
ActionCable.server.broadcast(
"chat_#{room}",
sent_by: 'Paul',
body: 'This is a cool chat app.'
)
Перетрансляция сообщения
Обычным сценарием является перетрансляция сообщения, посланного одним клиентом, любым другим подсоединенным клиентам.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
def receive(data)
ActionCable.server.broadcast("chat_#{params[:room]}", data)
end
end
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
received: (data) ->
# data => { sent_by: "Paul", body: "This is a cool chat app." }
App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
Перетрансляция будет получена всеми подсоединенными клиентами, включая клиента, отправившего сообщение. Отметьте, что params те же самые, что были при подписке на канал.
Полные примеры
Следующие шаги настройки общие для обоих примеров:
Пример 1: Появление пользователя
Вот простой пример канала, отслеживающего является ли пользователь онлайн или нет, и на какой он странице. (Это полезно для создания особенностей присутствия, наподобие зеленой точки рядом с именем пользователя, если он онлайн).
Создание канала появлений на сервере:
# 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
Когда инициализируется подписка, вызывается колбэк subscribed
, и мы имеем возможность сказать "определенно, текущий пользователь появился онлайн". Это API появления/исчезновения может быть реализовано в Redis, базе данных или еще как-нибудь.
Создание подписки на канал появлений на клиенте:
# app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create "AppearanceChannel",
# Вызывается, когда подписка готова на сервере для использования.
connected: ->
@install()
@appear()
# Вызывается, когда закрывается соединения WebSocket.
disconnected: ->
@uninstall()
# Вызывается, когда подписка отвергается сервером.
rejected: ->
@uninstall()
appear: ->
# Вызывает `AppearanceChannel#appear(data)` на сервере.
@perform("appear", appearing_on: $("main").data("appearing-on"))
away: ->
# Вызывает `AppearanceChannel#away` на сервере.
@perform("away")
buttonSelector = "[data-behavior~=appear_away]"
install: ->
$(document).on "turbolinks:load.appearance", =>
@appear()
$(document).on "click.appearance", buttonSelector, =>
@away()
false
$(buttonSelector).show()
uninstall: ->
$(document).off(".appearance")
$(buttonSelector).hide()
Клиент-серверное взаимодействие
Клиент соединяется с Сервером с помощью
App.cable = ActionCable.createConsumer("ws://cable.example.com")
. (cable.js
). Сервер идентифицирует экземпляр этого соединения поcurrent_user
.Клиент подписывается на канал появлений с помощью
App.cable.subscriptions.create(channel: "AppearanceChannel")
. (appearance.coffee
)Сервер распознает, что была инициализирована новая подписка для канала появлений, и запускает колбэк
subscribed
, вызывающий методappear
наcurrent_user
. (appearance_channel.rb
)Клиент распознав, что подписка была установлена, вызывает
connected
(appearance.coffee
), который, в свою очередь, вызывает@install
и@appear
.@appear
вызываетAppearanceChannel#appear(data)
на сервере и предоставляет хэш данныхappearing_on: $("main").data("appearing-on")
. Это возможно, так как экземпляр канала на сервере автоматически открывает публичные методы, объявленные в классе (кроме колбэков), таким образом, они достижимы для вызова в качестве удаленных процедур с помощью метода подпискиperform
.Сервер получает запрос для экшна
appear
на канале появлений для соединения, идентифицированногоcurrent_user
. (appearance_channel.rb
). Сервер получает данные с ключом:appearing_on
из хэша данных и устанавливает его в качестве значения для ключа:on
, передаваемого вcurrent_user.appear
.
Пример 2: Получение новых веб-уведомлений
Пример с появлением пользователей был об открытии серверного функционала для вызова на стороне клиента через соединение WebSocket. Но отличительная особенность WebSockets в том, что они двусторонние. Давайте покажем пример, когда сервер вызывает экшн на клиенте.
Это канал веб-уведомлений, позволяющий показать веб-уведомления на клиенте при трансляции в правильные потоки:
Создание канала веб-уведомлений на сервере:
# app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
Создание подписки на канал веб-уведомлений на клиенте:
# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
# На клиенте полагаем, что уже запросили право
# посылать веб-уведомления
App.cable.subscriptions.create "WebNotificationsChannel",
received: (data) ->
new Notification data["title"], body: data["body"]
Транслируем содержимое в экземпляр канала веб-уведомлений откуда-нибудь из приложения:
# Это вызывается где-то в приложении, возможно из NewCommentJob
WebNotificationsChannel.broadcast_to(
current_user,
title: 'New things!',
body: 'All the news fit to print'
)
Вызов WebNotificationsChannel.broadcast_to
помещает сообщение в очередь pubsub текущего адаптера подписки под отдельным именем трансляции для каждого пользователя. Для пользователя с ID 1, имя трансляции будет web_notifications:1
.
Канал проинструктирован писать в поток все, что приходит в web_notifications:1
, непосредственно на клиент, вызывая колбэк received
. Данные. передаваемые как аргумент, – это хэш, посылаемый в качестве второго параметра в вызов трансляции на сервере, кодируемый для передачи в JSON, и распакованный в аргументе data, приходящем в received
.
Больше полных примеров complete examples
Смотрите репозиторий rails/actioncable-examples, чтобы получить полный пример, как настроить Action Cable в приложении Rails и добавить каналы.
Настройка
У Action Cable есть две требуемые настройки: адаптер подписки и допустимые домены запроса.
Адаптер подписки
По умолчанию Action Cable ищет конфигурационный файл в config/cable.yml
. Этот файл должен указывать адаптер для каждой среды Rails. Подробности об адаптерах смотрите в разделе Зависимости.
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: redis://10.10.3.153:6381
channel_prefix: appname_production
Конфигурация адаптера
Ниже приведен список адаптеров подписки, доступных для конечных пользователей.
Адаптер async
Асинхронный адаптер предназначен для development/testing сред и не должен использоваться в production.
Адаптер Redis
Адаптер Redis требует от пользователей предоставления URL, указывающего на сервер Redis. Кроме того, может быть предоставлен channel_prefix
, чтобы избежать конфликта имен каналов при использовании одного и того же сервера Redis для нескольких приложений. Смотрите документацию Redis PubSub для получения дополнительной информации.
Адаптер PostgreSQL
Адаптер PostgreSQL использует пул подключений Active Record и, соответственно, конфигурацию базы данных приложения config/database.yml
для ее подключения. Это может измениться в будущем. #27214
Допустимые домены запроса
Action Cable принимает только запросы с определенных доменов, которые передаются в конфигурацию сервера массивом. Домены могут быть экземплярами строк или регулярных выражений, с которыми выполняется сверка.
config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}]
Чтобы отключить и, тем самым, разрешить запросы с любого домена:
config.action_cable.disable_request_forgery_protection = true
По умолчанию Action Cable позволяет все запросы из localhost:3000 при запуске в среде development.
Настройка потребителя
Чтобы сконфигурировать URL, добавьте вызов action_cable_meta_tag
в макете HTML HEAD. Он использует URL или путь, обычно устанавливаемые с помощью config.action_cable.url
в файлах настройки среды.
Другие настройки
Другой обычной опцией для настройки являются теги логирования, присоединяемые к логгеру для каждого соединения. Вот пример, использующий при тегировании идентификатор пользовательской записи при наличии, а в противном случае "no-account"
config.action_cable.log_tags = [
-> request { request.env['user_account_id'] || "no-account" },
:action_cable,
-> request { request.uuid }
]
Полный список всех конфигурационных опций смотрите в классе ActionCable::Server::Configuration
.
Также отметим, что ваш сервер должен предоставить как минимум то же самое количество соединений с базой данных, сколько у вас есть воркеров. Пул воркеров по умолчанию установлен 4, это означает, что нужно сделать доступными соединения как минимум для них. Это можно изменить в config/database.yml
с помощью атрибута pool
.
Запуск отдельного сервера cable
В приложении
Action Cable может быть запущен вместе с вашим приложением Rails. Например, чтобы слушать запросы WebSocket на /websocket
, укажите этот путь в config.action_cable.mount_path
:
# config/application.rb
class Application < Rails::Application
config.action_cable.mount_path = '/websocket'
end
Можно использовать App.cable = ActionCable.createConsumer()
, чтобы соединить с сервером cable, если action_cable_meta_tag
вызван в макете. Произвольный путь указывается в качестве первого аргумента createConsumer
(например, App.cable = ActionCable.createConsumer("/websocket")
).
Для каждого экземпляра создаваемого сервера и для каждого воркера, порождаемого сервером, у вас также будет новый экземпляр Action Cable, но использование Redis позволяет синхронизировать сообщения между соединениями.
Отдельное
Серверы cable могут быть отделены от обычного сервера приложений. Это все еще приложение Rack, но это отдельное приложение Rack. Рекомендуемая базовая настройка следующая:
# cable/config.ru
require_relative '../config/environment'
Rails.application.eager_load!
run ActionCable.server
Затем можно запустить сервер с помощью binstub в bin/cable
, наподобие:
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru
Вышесказанное запустит сервер cable на порту 28080.
Заметки
У сервера WebSocket нет доступа к сессии, но есть доступ к куки. Это можно использовать, если нужно обрабатывать аутентификацию. Один из способов с помощью Devise можно посмотреть в этой статье.
(dependencies) Зависимости
Action Cable предоставляет интерфейс адаптера подписки для обработки его pubsub внутренностей. По умолчанию включены адаптеры асинхронный, встроенный, PostgreSQL, и адаптеры Redis. В новых приложениях Rails по умолчанию используется асинхронный (async
) адаптер.
Часть Ruby этих вещей создана на основе websocket-driver, nio4r и concurrent-ruby.
Развертывание
Action Cable работает на комбинации WebSockets и тредов. Работа обоих фреймворка и определенного для пользователя канала, внутренне обрабатываются с помощью поддержки нативных тредов Ruby. Это означает, что вы можете без проблем использовать все обычные модели Rails, до тех пор, пока они отвечают тредобезопасности.
Сервер Action Cable реализует API сокетов Rack, тем самым позволяет внутренне использовать мультитредовый паттерн для управления соединениями, независимо от того, является ли сервер приложения многопоточным.
В соответствии с этим, Action Cable работает со популярными серверами, такими как Unicorn, Puma и Passenger.