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.js
的 received
回調方法:
//...
received: function(data) {
var msg = data['title'] + '\n' + data['body'] + '\n';
//簡單地打印接收到的信息
console.log(msg);
}
重啟服務器、重新訪問 localhost:3000/room/show
,每隔 3 秒就能看到打印信息。
客戶端主動向服務器發送消息
客戶端也可以主動調用服務器端在 channel 中定義的方法。
重寫 assets/javascripts/channels/room.js
的 connected
回調方法:
//...
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 。
參考文章:
- Action Cable Overview
- websocket序列文章
- Action Cable Source Code ,我翻了兩天的源碼。。。。