Rails ActionCable 基本概念及实践

lanzhiheng · 2022年04月12日 · 最后由 flask 回复于 2022年04月29日 · 1130 次阅读

这篇文章简单总结了一下 Rails 中 ActionCable 的基本概念及实践。原文链接:https://step-by-step.tech/posts/basic-concept-and-usage-for-actionable


使用 Ruby On Rails 编写任何 Web 相关的应用程序都比想象中简单,其中也包括 Websocket 相关功能。为了提供 Websocket 服务,Rails 官方提供了ActionCable这一套工具集。能够快速实现 Pub/Sub 模型。

ActionCable 基本概念

ActionCable 其实是对 Websocket 所做的 Ruby 封装,让我们能够很方便地去实现 Websocket 相关的功能。然而 ActionCable 本身包含了不少概念

  • Connection:连接
  • Consumers:消费者
  • Channels:渠道
  • Subscribers:订阅者
  • Pub/Sub:发布-订阅模型
  • Broadcastings:广播

官方文档对上面的概念有十分详尽的描述,这里就不赘述了,我就用个比较形象的例子来说明。可以想象一下我们平时使用微信进行聊天的过程。

当我们打开微信的时候实际上就相当于作为一个 Consumer(消费者)创建了个人与微信服务器之间的一个 Connection(连接)。接下来我们通过微信群跟其他人进行聊天,则相当于我们订阅了该微信群,微信群则相当于 Channel(渠道),而我们则是这个渠道的 Subscriber(订阅者)。当群里有人说话的时候,微信服务器则向所有订阅了该渠道的组群成员推送信息,这个过程称之为 Broadcasting(广播),然后所有在群里的人都能够收到信息。我们跟微信群的关系可以理解成 Pub/Sub(发布 - 订阅模型)。

另一个值得提一下的就是,我们只需要打开一次微信,就可以参加多个微信群的群聊。相当于我们作为一个消费者,只需要创建一个连接,却可以订阅多个渠道,接收来自不同渠道的广播,同时我们也可以将自己要说的话广播给其他人。

这样一类比会不会对 ActionCable 的运作模式有了一个初步的了解了呢?

ActionCable 服务端实现

根据上面描述的基本概念,假设我们需要为客户端提供 Websocket 服务,那么我们需要向用户提供的东西主要有

  1. 可供用户创建连接的 URL。
  2. 可供用户订阅的渠道。

这两者在 ActionCable 中实现可以说超级简单。

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    # 连接创建的时候会调用
    def connect
      # connect
    end
  end
end

这个方法在建立 Websocket 连接的时候会调用,从前面跟微信的类比可以看出,Websocket 服务自始自终只需要建立一次连接(不包括断开重连)。却可以通过订阅不同的渠道来得到来自服务端的不同推送。接下来看看渠道的实现。

1. 渠道单一推送

在 ActionCable 里面,不同的渠道都继承自ApplicationCable::Channel。假设我们网站首页需要接收来自服务端的推送,那么渠道可以简单地这样去实现

# app/channels/home_channel.rb

class HomeChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'home_page'
  end
end

建立连接(客户端代码后面再提供)并订阅HomeChannel这个渠道之后,客户端便可获取来自服务端的推送。当服务端需要往客户端推送数据的时候可以

ActionCable.server.broadcast('home_page', {
                               action: 'create_tender',
                               price: 1000
                             })

HomeChannel#subscribed中已经定义好,客户端会接收来自于拥有home_page这个标识的渠道的相关推送。数据集可以自定义(这个可以事先沟通好)。

2. 渠道区分资源推送

前面采用stream_from方法指定了home_page标识的推送,这种模式常用于较为通用的信息推送。比方说,网站就只有一个首页,所有人的首页都统一订阅HomeChannel这个渠道,并得到相同的推送信息。这跟前面的微信群聊稍微有些不同,因为不同的聊天室推送的内容肯定有所区别,我们又不可能为每一个微信群都创建一个xxxChannel的渠道文件来对应不同的微信群,这样就有成千上万个 Channel 文件需要维护了。我们拿常见的直播间举例,当用户加入不同的直播间会以直播间的唯一标识来订阅该直播间渠道,并获得来自该直播间的推送。那么服务端代码大概是

# app/channels/studio_channel.rb

class StudioChannel < ApplicationCable::Channel
  def subscribed
    studio = Studio.find(params[:id])
    stream_for studio
  end
end

接下来我们就可以通过不同的直播间 id,来订阅指定的直播间渠道,以实现不同的直播间可以推送不一样的内容。推送代码如下

> @studio = Studio.find(1)
> StudioChannel.broadcast_to(@studio, {
                               action: 'join'
                               users_count: @studio.users_count,
                               user_name: 'Ruby'
                             })

这条信息只往id=1的直播间推送,推送的数据可以自由定制,跟前端协调好即可,其他的直播间不会受到影响。

从上面可以了解到 ActionCable 的服务端基本用法,简单概括就是

  1. 提供连接及渠道让客户端可以访问并订阅。
  2. 服务端往渠道推送消息,客户端可以接收来自服务端的推送并处理数据。

接下来看看客户端要如何创建连接并订阅渠道。

ActionCable 客户端实现

谈了服务端的用法,接下来谈谈客户端该如何跟服务端交互。如果只是在浏览器上面使用的 Web 页面,其实使用官方的工具包是最简单的。而如果客户端是小程序/App,那么可能要自己“手写 SDK”。需要窥探一下源码,其实也不难。

1. 官方工具包

先讲讲如何用官方的工具包来实现跟 ActionCable 服务端交互。

import { createConsumer } from "@rails/actioncable"

const customer = createConsumer('wss://www.example.com/cable') // ActionCable默认连接路径都是/cable。

// 订阅id=1的直播间

const channel = consumer.subscriptions.create({ channel: "StudioChannel", id: 1 }, {
  // 接收推送信息
  received(data) {
    // 服务端运行`StudioChannel.broadcast_to(@studio, { hello: 1 })`后,这里会打印出 { hello: 1 }
    console.log(data)
  }
})

前面保存了channel这个变量,我们可以在程序的某处取消对该渠道的订阅

channel.unsubscribe();

2. 一般用法

从上面的例子可以看出,采用官方的工具包完成对 ActionCable 渠道的订阅并获取推送信息是如此的简单。然而在很多场景下,其实我们并没有办法直接使用官方工具包(比方说开发小程序/App 的时候)。在这种场景下我们只能自行研发工具包了。窥探一下官方源代码其实很容易写出来,这里只提供 JavaScript 的代码,其他语言大同小异。

window.websocket = new WebSocket('wss://www.example.com/cable')

// 唯一标识
const identifier = JSON.stringify({
  channel: 'StudioChannel',
  id: 1
})

// 接收推送信息
websocket.onmessage = function(data) {
  console.log(data)
}

websocket.onopen = function(data) {
  // 订阅
  window.subscription = websocket.send(JSON.stringify({
    command: 'subscribe',
    identifier
  }))
}

// 取消订阅
websocket.send(JSON.stringify({
  command: 'unsubscribe',
  identifier
}))

代码还算比较简单,简单概括就是把参数组装成唯一标识,进行订阅并设置消息接收回调,在某些场景下还能取消对该渠道的订阅。

PS: WebSocket 是单一连接 + 多渠道订阅的,在一些场景下容易断连。需要根据实际场景实现重连逻辑。否则断连后客户端将收不到任何消息推送。

尾声

这篇文章简单总结了一下 Rails 中 ActionCable 的基本概念及实践。其中包括如何用 ActionCable 开发出一个简单的 Websocket 服务,并推送消息。另外,客户端也能够使用恰当的方式连接该服务,官方提供了简易的工具包可以实现这一点,然而这个工具包对于很多场景(小程序/App)并不是很适用,于笔者窥探了一下工具包的源码,并提供了一个暴露原理的 JavaScript 版本,其他语言也可以参考这个范例,以连接基于 ActionCable 的 Websocket 服务。

不错,先收藏着

这个 ActionCable 服务端是怎么样集成的?需要单独的一个进程服务吗?

关注他好久了,mark

niceruby 回复

我记得是不用,但可以单独起

niceruby 回复

大多时候我们是直接挂载到 rails 服务上直接使用的,rails 服务进程关掉了,cable 也就关掉了,也可以单独启动到一个进程服务中,也可以单独部署在一台服务器上

niceruby 回复

不用吧。就在 Rails 自带的。跟 Sidekiq 那种第三方的不一样。不过久了可能会有性能问题。

可以起在外部,但是目前我用这个 cable 大部分场景都是作为业务的延伸,比如说完成任务发个通知什么的,单独起对于我的作用不是很大。

想起来有一个 AnyCable,能解决性能问题

需要 登录 后方可回复, 如果你还没有账号请 注册新账号