试用 Action Cable

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

参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容