Rails Websocket 後端實作紀錄

👀 4 min read 👀

大家好,我是 Cindy,最近有做了 Rails Websocket 的功能,想說做個紀錄,以下文章重點會放在實作面,雖然是這麼說,但還是查一下什麼是 Websocket 好像比較正確。

Websocket

WebSocket 是一種網路傳輸協定,簡單講就是早期網路通訊只考慮到由 client 端發送請求給 server 端,這種單向的傳輸,當有雙向傳輸的需求(ex: 推播功能)實作上會變得比較複雜,所以後來就出現 WebSocket 的通訊協定了!(當然詳細過程並不是這麼簡單 XD),可以參考維基百科的說明。

Rails 6 實作 Websocket

在 Rails 5 之後有了 ActionCable 讓我們可以更方便的在 Rails 中做 Websocket 的應用。

  1. 首先 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
      3
      rails generate model User name email password_digest
      rails db:create
      rails db:migrate
    • 修改 User model
      1
      2
      3
      4
      5
      6
      class 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")
  2. 打開 config/cable.yml,修改 adapter,參考文件,我們可以使用 redis 當作 adapter。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    development:
    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
  3. 打開 config/application.rb,新增 ActionCable 的 mount path,也就是要進行 Websocket 連線時的 path。

    1
    config.action_cable.mount_path = '/cable'
  4. 打開 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
  5. 打開 config/environments/production.rb,設定 allowed request origins。(有需要跨不同源網站互動的時候再設定)

    1
    config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
  6. 打開 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
    21
    module 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
  7. 新增一個 channel

    1
    rails g channel test
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class TestChannel < ApplicationCable::Channel
    def subscribed
    stream_from 'test_channel'
    end

    def unsubscribed
    stop_all_streams
    end
    end
  8. 簡單地進行測試
    先在 terminal 輸入 rails s,將 Rails server run 起來,接著可以在瀏覽器 DevTools 的 console 使用 WebSocket APIpostman 進行 websocket 連線,接著進行頻道的訂閱

    1
    2
    3
    4
    {
    "command": "subscribe",
    "identifier": "{\"channel\":\"TestChannel\"}"
    }

    rails log 如下表示有成功訂閱 test 頻道

    1
    2
    TestChannel 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
    5
    document.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 環境,也是會遇到不同源的問題。

參考資料