Rails Websocket 後端實作紀錄
大家好,我是 Cindy,最近有做了 Rails Websocket 的功能,想說做個紀錄,以下文章重點會放在實作面,雖然是這麼說,但還是查一下什麼是 Websocket 好像比較正確。
Websocket
WebSocket 是一種網路傳輸協定,簡單講就是早期網路通訊只考慮到由 client 端發送請求給 server 端,這種單向的傳輸,當有雙向傳輸的需求(ex: 推播功能)實作上會變得比較複雜,所以後來就出現 WebSocket 的通訊協定了!(當然詳細過程並不是這麼簡單 XD),可以參考維基百科的說明。
Rails 6 實作 Websocket
在 Rails 5 之後有了 ActionCable 讓我們可以更方便的在 Rails 中做 Websocket 的應用。
首先 new 一個新的 Rails 專案,並進行前置作業
1
rails new test-action-cable-api --database=postgresql -T --api
在 output 中其實就會看到 rails 自動幫我們產生了一些跟 ActionCable 有關的檔案
1
2
3
4
5
6
7...
create app/channels/application_cable/channel.rb
create app/channels/application_cable/connection.rb
...
create config/cable.yml
...- 建立 User model & db schema
1
2
3rails generate model User name email password_digest
rails db:create
rails db:migrate - 修改 User model
1
2
3
4
5
6class User < ApplicationRecord
has_secure_password
validates :name, presence: true
validates :email, presence: true, uniqueness: true
end - 在
Gemfile
uncomment redis 和 bcrypt,接著執行 bundle install 進行套件安裝1
2
3
4# Use Redis adapter to run Action Cable in production
gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7' - 在 rails c 先 create 一個測試的 user
1
User.create!(name: "cindy", email: "cindy@test.com", password: "12345678")
- 建立 User model & db schema
打開
config/cable.yml
,修改 adapter,參考文件,我們可以使用 redis 當作 adapter。1
2
3
4
5
6
7
8
9
10
11development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: test_action_cable_api_production打開
config/application.rb
,新增 ActionCable 的 mount path,也就是要進行 Websocket 連線時的 path。1
config.action_cable.mount_path = '/cable'
打開
config/environments/development.rb
,在開發環境將同源限制移除。1
2# Uncomment if you wish to allow Action Cable access from any origin.
config.action_cable.disable_request_forgery_protection = true打開
config/environments/production.rb
,設定 allowed request origins。(有需要跨不同源網站互動的時候再設定)1
config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
打開
app/channels/application_cable/connection.rb
,作為建立 connection 時身份認證使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
# 通常不太會直接把 user id 放在 cookies,只是方便暫時這樣寫歐
# 暫時測試時也可以先直接寫死 id
def find_verified_user
if verified_user = User.find_by(id: cookies[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end新增一個 channel
1
rails g channel test
1
2
3
4
5
6
7
8
9class TestChannel < ApplicationCable::Channel
def subscribed
stream_from 'test_channel'
end
def unsubscribed
stop_all_streams
end
end簡單地進行測試
先在 terminal 輸入 rails s,將 Rails server run 起來,接著可以在瀏覽器 DevTools 的 console 使用 WebSocket API 或 postman 進行 websocket 連線,接著進行頻道的訂閱1
2
3
4{
"command": "subscribe",
"identifier": "{\"channel\":\"TestChannel\"}"
}rails log 如下表示有成功訂閱 test 頻道
1
2TestChannel is transmitting the subscription confirmation
TestChannel is streaming from test_channel接著在 rails console 用以下程式碼發送通知到 test channel
1
ActionCable.server.broadcast('test_channel', 'test')
rails log
1
TestChannel transmitting "test" (via streamed from test_channel)
postman 會收到訊息
如此一來就可以將 broadcast 寫在需要發送訊息到 channel 的任何地方囉(通常可能是 Job 裡)。
另外若使用 js WebSocket API 加上 cookies 測試範例如下:
1
2
3
4
5document.cookie = 'user_id=' + 1 + '; path=/';
var ws = new WebSocket(
'ws://localhost:3000/cable'
);從 Network 觀看結果
需要注意的地方
- 在做身份認證的其中一個方式是可以放 token 在 header,但對瀏覽器來說用 JavaScript 在 Websocket 傳送客製化的 header 是困難的,參考: HTTP headers in Websockets client API。
- 用 cookie 做身份認證的話,如果說前後端不同源也是會有問題。
- 如果用 postman 測試 production 環境,也是會遇到不同源的問題。