跳至内容 跳至搜索

Action Cable 通道基础

在通过 WebSocket 连接进行通信时,通道提供了将行为分组到逻辑单元的基本结构。可以将通道视为一种控制器形式,但它除了可以响应订阅者的直接请求外,还可以向订阅者推送内容。

Channel 实例是长期存在的。当电缆使用者成为订阅者时,将实例化一个通道对象,然后一直存在到使用者断开连接为止。这可能需要几秒、几分钟、几小时甚至几天。这意味着必须特别注意不要在通道中执行任何会增加其内存占用或其他任何操作的愚蠢操作。引用是永久的,因此不会像在每次请求后被丢弃的控制器实例那样被释放。

长期存在的通道(和连接)还意味着你负责确保数据是最新。如果你持有对用户记录的引用,但在持有该引用时名称发生了更改,那么如果不采取预防措施来避免这种情况,你可能会发送过时的数据。

长期存在的通道实例的好处是,你可以使用实例变量来保留对未来订阅者请求可以与之交互的对象的引用。以下是一个快速示例

class ChatChannel < ApplicationCable::Channel
  def subscribed
    @room = Chat::Room[params[:room_number]]
  end

  def speak(data)
    @room.speak data, user: current_user
  end
end

speak 操作只是使用 Chat::Room 对象,该对象是在使用者首次订阅通道时创建的,当时该订阅者希望在房间内说些什么。

操作处理

ActionController::Base 的子类不同,通道不会遵循其操作的 RESTful 约束形式。相反,Action Cable 通过远程过程调用模型进行操作。你可以在通道上声明任何公共方法(可以选择采用 data 参数),并且该方法会自动向客户端公开为可调用方法。

示例

class AppearanceChannel < ApplicationCable::Channel
  def subscribed
    @connection_token = generate_connection_token
  end

  def unsubscribed
    current_user.disappear @connection_token
  end

  def appear(data)
    current_user.appear @connection_token, on: data['appearing_on']
  end

  def away
    current_user.away @connection_token
  end

  private
    def generate_connection_token
      SecureRandom.hex(36)
    end
end

在此示例中,已订阅和已取消订阅方法不是可调用方法,因为它们已在 ActionCable::Channel::Base 中声明,但 #appear#away 是可调用方法。#generate_connection_token 也不是可调用方法,因为它是一个私有方法。您会看到 appear 接受一个数据参数,然后将其用作其模型调用的部分。#away 不接受,因为它只是一个触发器操作。

还要注意,在此示例中,current_user 可用,因为它被标记为连接上的标识属性。所有此类标识符都将在频道实例上自动创建一个同名委托方法。

拒绝订阅请求

频道可以在 subscribed 回调中通过调用 reject 方法来拒绝订阅请求

class ChatChannel < ApplicationCable::Channel
  def subscribed
    @room = Chat::Room[params[:room_number]]
    reject unless current_user.can_access?(@room)
  end
end

在此示例中,如果 current_user 无权访问聊天室,则将拒绝订阅。在客户端,当服务器拒绝订阅请求时,将调用 Channel#rejected 回调。

方法
A
C
D
E
M
N
P
R
S
T
U
包含的模块

属性

[R] connection
[R] identifier
[R] params

类公共方法

action_methods()

应视为操作的方法名称列表。这包括频道上的所有公共实例方法,减去任何内部方法(在 Base 上定义),重新添加任何内部方法,但仍存在于类本身中。

返回

  • Set - 应视为操作的所有方法的集合。

# File actioncable/lib/action_cable/channel/base.rb, line 120
def action_methods
  @action_methods ||= begin
    # All public instance methods of this class, including ancestors
    methods = (public_instance_methods(true) -
      # Except for public instance methods of Base and its ancestors
      ActionCable::Channel::Base.public_instance_methods(true) +
      # Be sure to include shadowed public instance methods of this class
      public_instance_methods(false)).uniq.map(&:to_s)
    methods.to_set
  end
end

new(connection, identifier, params = {})

# File actioncable/lib/action_cable/channel/base.rb, line 147
def initialize(connection, identifier, params = {})
  @connection = connection
  @identifier = identifier
  @params     = params

  # When a channel is streaming via pubsub, we want to delay the confirmation
  # transmission until pubsub subscription is confirmed.
  #
  # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
  @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)

  @reject_subscription = nil
  @subscription_confirmation_sent = nil

  delegate_connection_identifiers
end

类私有方法

clear_action_methods!()

action_methods 已缓存,有时需要刷新它们。 ::clear_action_methods! 允许您这样做,因此下次运行 action_methods 时,它们将被重新计算。

# File actioncable/lib/action_cable/channel/base.rb, line 136
def clear_action_methods! # :doc:
  @action_methods = nil
end

method_added(name)

添加新的 action_method 时刷新缓存的 action_methods

# File actioncable/lib/action_cable/channel/base.rb, line 141
def method_added(name) # :doc:
  super
  clear_action_methods!
end

实例公共方法

perform_action(data)

从传递的数据中提取动作名称,并通过频道处理它。此过程将确保请求的动作是用户声明的频道上的公共方法(因此不是像 subscribed 这样的回调之一)。

# File actioncable/lib/action_cable/channel/base.rb, line 167
def perform_action(data)
  action = extract_action(data)

  if processable_action?(action)
    payload = { channel_class: self.class.name, action: action, data: data }
    ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
      dispatch_action(action, data)
    end
  else
    logger.error "Unable to process #{action_signature(action, data)}"
  end
end

subscribe_to_channel()

在订阅已添加到连接并确认或拒绝订阅后调用此方法。

# File actioncable/lib/action_cable/channel/base.rb, line 182
def subscribe_to_channel
  run_callbacks :subscribe do
    subscribed
  end

  reject_subscription if subscription_rejected?
  ensure_confirmation_sent
end

实例私有方法

defer_subscription_confirmation!()

# File actioncable/lib/action_cable/channel/base.rb, line 233
def defer_subscription_confirmation! # :doc:
  @defer_subscription_confirmation_counter.increment
end

defer_subscription_confirmation?()

# File actioncable/lib/action_cable/channel/base.rb, line 237
def defer_subscription_confirmation? # :doc:
  @defer_subscription_confirmation_counter.value > 0
end

ensure_confirmation_sent()

# File actioncable/lib/action_cable/channel/base.rb, line 227
def ensure_confirmation_sent # :doc:
  return if subscription_rejected?
  @defer_subscription_confirmation_counter.decrement
  transmit_subscription_confirmation unless defer_subscription_confirmation?
end

reject()

# File actioncable/lib/action_cable/channel/base.rb, line 245
def reject # :doc:
  @reject_subscription = true
end

subscribed()

消费者成为频道订阅者后调用一次。通常是设置希望此频道发送给订阅者的任何流的地方。

# File actioncable/lib/action_cable/channel/base.rb, line 202
def subscribed # :doc:
  # Override in subclasses
end

subscription_confirmation_sent?()

# File actioncable/lib/action_cable/channel/base.rb, line 241
def subscription_confirmation_sent? # :doc:
  @subscription_confirmation_sent
end

subscription_rejected?()

# File actioncable/lib/action_cable/channel/base.rb, line 249
def subscription_rejected? # :doc:
  @reject_subscription
end

transmit(data, via: nil)

向订阅者传输一个数据哈希。该哈希将自动封装在一个 JSON 信封中,并标记为接收者的适当频道标识符。

# File actioncable/lib/action_cable/channel/base.rb, line 214
def transmit(data, via: nil) # :doc:
  logger.debug do
    status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
    status += " (via #{via})" if via
    status
  end

  payload = { channel_class: self.class.name, data: data, via: via }
  ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
    connection.transmit identifier: @identifier, message: data
  end
end

unsubscribed()

消费者断开电缆连接后调用一次。可用于清理连接或将用户标记为离线或类似情况。

# File actioncable/lib/action_cable/channel/base.rb, line 208
def unsubscribed # :doc:
  # Override in subclasses
end