[Rails] Actioncable 即時通訊

Actioncable是一個Pub/Sub 模型 + WebSocket 的 Ruby 框架,可以讓Rails透過websocket,實現即時通訊。

WebSocket

WebSocket是一個基於TCP的應用層協議,通訊過程是透過向HTTP標頭添加特定信息,然後發送到伺服器,如果伺服器能夠支持的WebSocket的話,就會識別出HTTP標頭中關於WebSocket的資訊,並將HTTP連線升級為WebSocket的連接並返回一個同樣包含websocket標頭資訊的http回應。客戶端和伺服器就可以透過這個已經建立的連線,進行雙向的通訊,而不需要不斷對資料更改進行輪詢(polling)。

輪循(Polling)

Polling是指利用ajax每隔一段時間就向服務器發送請求,例如,股票數據的即時更新。這種方案會頻繁地與服務器通訊,很消耗網路資源。

Pub/Sub 發佈/訂閱模式 (Publish/Subscribe Pattern)

有別於傳統的客戶/服務器模式,在Pub / Sub模型中,Publisher(消息的發布者)和Subscriber(消息的訂閱者)之間沒有關聯,而是藉助中間人 (broker)進行通訊。大部分 Pub/Sub 透過非同步(async) 的方式傳遞訊息,使訊息的傳遞不需等待回應,可以繼續後續的操作。

附帶有 Pub/Sub 功能的資料庫:

  • Redis
  • PostgreSQL

Actioncable 實作

  • 建立專案 chatapp
$ rails new chatapp
  • 建立rooms controller
$ rails g controller rooms show
  • 修改 routes.rb
Rails.application.routes.draw do
root to: 'rooms#show'
end
  • 建立 message model
$ rails g model message content:text
$ rails db:migrate
  • 修改rooms controller
class RoomsController < ApplicationController
def show
@messages = Message.all
end
end
  • 先建立一筆 message 資料
$ rails c
irb(main):001:0> Message.create!(content: "Hello World!")
  • 建立單獨 message 的 partial
# views/messages/_message.html.erb
<div class="message">
<
p><%= message.content %></p>
</div>
  • 更新 rooms/show.html.erb 讓 messages 顯示
<h1>Chat room</h1>
<
div id="messages">
<%= render
@messages %>
</
div>
  • Actioncable 建立
$ rails g channel room speak
create app/channels/room_channel.rb
identical app/assets/javascripts/cable.js
create app/assets/javascripts/channels/room.coffee
  • Mount ActionCable
# config/routes.rb
Rails.application.routes.draw do
...
mount ActionCable.server => '/cable'
end
  • run rails server

設定完成後,如果在瀏覽器的console 輸入App.cable 會看到 rails server 顯示 “RoomChannel is transmitting the subscription confirmation”,表示用戶端已經連線到伺服器端。

  • 爲了讓用戶端可以真的發佈 messages,修改 room.coffee 的 speak function:
// app/assets/javascripts/channels/room.coffee
...略
speak: (message)->
@perform 'speak', message: message
  • 修改 speak action:
# 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
  def speak data
ActionCable.server.broadcast "room_channel", message: data['message']
end
end
  • 修改 room.coffee,讓server在接受到訊息後執行反應
received: (data) ->
alert data['message']
  • 重啓 server

因爲客戶端的連線未關閉,所以連線還是存在。

  • 在瀏覽器的console 執行 App.room.speak("Hello, World!") 就會看到彈出視窗,表示伺服器端已經接受到訊息。
  • 建立溝通界面
# show.html.erb
<h1>Chat room</h1>
<
div id="messages">
<%= render
@messages %>
</
div>
<form>
<
label>Say something:</label>
<
input type="text" data-behavior="room_speaker">
</
form>
  • 表單動作
// room.coffee
...
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13 #return
App.room.
speak event.target.value
event.target.
value = ''
event.
preventDefault()

現在,使用者可以透過界面傳送訊息到伺服器了。但問題是message並沒有真的存入資料庫中,接下來要實作資料庫的部分。

註:需裝有jquery。

安裝 jquery:

gem 'jquery-rails'bundle install

在application.js 載入

//= require jquery
  • 實際存取database:修改room_channel.rb
def speak data
Message.
create!(content: data['message'])
end
  • 利用Rails Job 在message建立之後Render message partial 到頁面:
# app/models/message.rb
class Message < ApplicationRecord
after_create_commit { MessageBroadcastJob.perform_later self }
end
  • 建立 Job
$ rails g job MessageBroadcast
  • 修改 job
# app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def
perform(message)
ActionCable.
server.broadcast "room_channel", message: render_message(message)
end
private
def
render_message message
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
end
end
  • 測試Job是否正確運作

已經正確產生了partial,但 message 沒有即時顯示在頁面上。

  • 修改room.coffee的received function
received: (data) ->
$('#messages').append data['message']

現在頁面上已經可以即時顯示 message了!在頁面上送出訊息時,可以看到伺服器執行了完整的流程:

  • 完成!

現在可以嘗試同時打開2個視窗,在其中一個視窗送出訊息時,2個視窗將會同時接收到訊息。

完整程式碼

參考: