试用 Action Cable

2016-08-02 jude 更多博文 » 博客 » GitHub »

ruby rails websocket action cable

原文链接 http://judes.me/ruby/2016/08/02/try-actioncable.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


Action Cable 有什么用

Action Cable 是一项满足客户端与服务器端实时通讯需求的功能,它基于 WebSocket 协议。在此之前 web 端要满足类似的需求,有 轮询、长轮询、SSE(Server Sent Events ,sinatra 自带一个简单的实现,有兴趣可以看看) 等方法,综合考虑开销和兼容性,基于 WebSocket 的实现是最好的。

WebSocket 的基本知识

websocket 是建立在 TCP 协议上面应用层的协议,整个协议由两部分组成: 握手建立连接,数据传输。要建立 websocket 连接,得先由客户端发送一个 http get 请求,带上相关的请求头,只有当服务器端带上正确的响应头回复时,连接才能建立。之后客户端和服务器端可以向对方发送数据。

更详细的说明,可以看这里

如果想简单地动手玩玩 websocket ,请参考这篇文章

Action Cable 基础概念

用户打开的每个浏览器标签页都会跟服务器建立一条新连接(connection),Rails 会为这条连接实例化一个 connection 对象,这个对象负责管理此后发生的订阅事件,它不处理具体的业务逻辑。

客户端可以通过一个连接订阅多个频道(channel)。每个频道都提供多个 websocket 的回调方法,方便写业务逻辑代码。

一个频道可以包含一个或多个流(stream),如果把频道比作网络游戏平台中的某个分区,那么流就是分区下面的某个房间。流是 Action Cable 中发送、接收消息的最小单位。

服务器可以在建立连接时设置验证(用异步的方式),一旦验证失败,将关闭已建立的连接。

整个 Action Cable 的架构粗略看起来就是下面的样子:

connections  <==   channels  <==   streams <--> subscriptions  ==>   connections

<== 表示 一 对 多 的关系

<--> 表示 一对一 的关系

==> 表示 多对一 的关系

Action Cable 基本配置

以下代码、说明仅在 Rails 5.0.0 版本(不是 beta 版本)测试过,不同版本间 Action Cable 的表现有稍许区别。

既然 websocket 需要由客户端发起(握手请求),先从前端需要做的事情说起。

运行 rails new my_actioncable 新建一个 Rails 项目。

app/assets/javascripts/cable.js ,Rails 已经替你准备好前端的 connection 实例:

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

为方便调试,你可以添加一行启用调试的代码:

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();
  ActionCable.startDebugging(); // 启用调试

}).call(this);

连接创建好之后,接着创建一个订阅。在 assets/javascripts/channels 目录下新建 room.js 文件,内容如下:

App.room = App.cable.subscriptions.create("RoomChannel", {
  connected: function(){
    // Called when the subscription is ready for use on the server
    },
  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },

  received: function(data) {
    // Called when there's incoming data on the websocket for this channel
  }
});

create 方法第一个参数可以是字符串,也可以是对象;如果是字符串,它表示要订阅的频道,如果是对象,则一定要带有 key 为 channel 的字段,其他字段可以传给后台别作它用(比如创建流),如:

{
  channel: 'RoomChannel',
  label: '1st'
}

create 方法第二个参数包含一系列的回调方法,各自用途注释都写得很清楚。

前端的事情就做完了,开始设置后台。

Action Cable 可以独立于我们的应用运行,也可以作为引擎挂载到我们的应用中。这里我们选择挂载。

routes.rb 添加一行:

mount ActionCable.server => '/cable'

这样发向 '/cable' 的请求将由 Action Cable 处理。

app/channels 目录下新建 room_channel.rb ,内容如下:

class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'room_channel'
  end
  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

steam_from 新建一个流,如果前端在调用 App.cable.subscriptions.create 时第一个参数是对象,可以通过 params 来获取对象的内容:

# 假设 第一个参数是对象
# {
#   channel: 'RoomChannel',
#   label: '1st'
# }

def subscribed
  stream_from "room:#{params[:label]}"
end

运行 rails g controller room show 生成控制器、路由和一个简单的 show 页面。

现在前后台都搭好了,得到一个最简单,什么都不能做的 Action Cable 。运行 rails s 启动服务器端,打开浏览器访问 localhost:3000/room/show

在浏览器的控制台可以看到类似以下的信息:

[ActionCable] Opening WebSocket, current state is null, subprotocols: actioncable-v1-json,actioncable-unsupported 1470455374056 action_cable.self-1641ec3….js?body=1:50 
[ActionCable] ConnectionMonitor started. pollInterval = 3000 ms 1470455374063 action_cable.self-1641ec3….js?body=1:50 
[ActionCable] WebSocket onopen event, using 'actioncable-v1-json' subprotocol 1470455374081 action_cable.self-1641ec3….js?body=1:50 
[ActionCable] ConnectionMonitor recorded connect 1470455374082

切换到控制台的 Network 标签,查看 WebSockets ,可以看到浏览器每隔 3 秒会收到服务器端发过来的 ping 包。

服务器主动向客户端推送消息

服务器可以主动通过广播向客户端推送消息。

为免阻塞正常的 http 响应,通常会采用 delayed job 来向客户端推送消息。

运行命令 rails g job send_msg 新建一个 delayed job ,在新建的 send_room_msg_job.rb 中的 perform 方法中添加:

# 每 3 秒向客户端发送一条信息
1.upto(10) do |i|
  sleep 3
  ActionCable.server.broadcast(
      'room_channel', # 这是流的名字,要跟在 stream_from 定义的保持一致
      title: 'the title',
      body: "server send #{i}"
  )
end

然后在 RoomController#show 方法中添加:

SendRoomMsgJob.perform_later

客户端接收部分,重写 assets/javascripts/channels/room.jsreceived 回调方法:

//...

received: function(data) {
  var msg = data['title'] + '\n' + data['body'] + '\n';
  //简单地打印接收到的信息
  console.log(msg);
}

重启服务器、重新访问 localhost:3000/room/show ,每隔 3 秒就能看到打印信息。

客户端主动向服务器发送消息

客户端也可以主动调用服务器端在 channel 中定义的方法。

重写 assets/javascripts/channels/room.jsconnected 回调方法:

//...

connected: function(){
  this.perform('print_log', { msg: 'send from client' });
}

room_channel.rb 中添加一个 print_log 方法:

#...

def print_log(data)
  p ">>>> #{data['msg']}"
end

只要连接一建立,就可以在服务器后台看到打印 >>>> message from client

以上就是简单的 Action Cable 试用记录,源码已经上传至 github

参考文章: