26
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RailsのActionCableと Nuxt.jsでリアルタイム更新アプリケーションの構築

Posted at

はじめに

株式会社diffeasyの政栄です。主にバックエンド担当です。(Rails)

弊社では「大会運営 向上心」という大会運営をサポートするwebアプリを開発・運営しています。

今回はその「大会運営 向上心」のトーナメント表示部分にてリアルタイムに更新されるようにしてみました。
※まだテスト段階で、リリース日は未定です。

実装概要

RailsのAction Cableを使用して、Websocket通信させて、リアルタイム更新を実現します!

また、Action CableやWebsocket関連の知識について未熟ですので、間違い等ありましたら、教えてください。

トーナメント表示にリアルタイム更新機能実装しよう!

トーナメント表示機能の説明

トーナメントは種目毎に見れるようになっています。
 2018-12-19 21.13.23.png

トーナメント機能では、トーナメントを見る人の画面と、トーナメント結果入力者用の画面があります。
 2018-12-19 21.19.31.png

現在は、結果入力が実施されたとき、閲覧者自が"最新の状態に更新"ボタンをクリックして更新があるかどうか確認しなければいけない状態です。

そのため、Action Cableを使用して、サーバー側から更新できるような仕組みにいこうというのが今回の試みです。

実装結果はこんな感じです。右が結果入力する人の画面で、左側が閲覧者の画面です。
トーナメントの右上の五郎さんが決勝へ進み、決勝戦で太郎さんが勝っています。

tournament4.gif

リアルタイム更新について

今回採用したAction CableではWebsocketという技術をつかっています。
実装前に使用されている技術について調べたことを載せます。

Websocketとは??

  • TCP上で低コストでの双方向通信
    低コストとは、昔の技術で、リアルタイム更新のような機能を実現しようとすると何秒毎に更新する(ポーリング)とかをして負荷をかけてた。websocketは不必要な更新しない。
  • websocketの通信手順
    1. HTTP(厳密にはそれをwebsocketにupgradeしたもの)でクライアントとサーバー間で情報をやり取りしてコネクション確立
    2. 確立されたコネクション上で、低コストな双方向通信。コネクションが確立されることをハンドシェイクという。握手

Railsでのwebsocket

Rails標準ではredisを使ってるメッセージキューをredisに保存
アクセス多いサイトになってきたら、別途redisサーバーを用意したりできる

Pub/Subについて

Pub/Subはパブリッシャ-サブスクライバ(pub/sub)型モデルとも呼ばれる、メッセージキューのパラダイムです。パブリッシャ側(Publisher)が、サブスクライバ側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用しています。

実装方法

アプリケーションの環境を載せます。

  • Rails (5.0.6)
    • ※Action CableはRails5からの機能です。
  • Nuxt.js (1.3.0)

バックエンド側の処理から

consoleにて

rails g channel message

チャンネルの設定

app/channels/message_channel.rb

  # チャンネル接続時に呼ばれる
  def subscribed
    stream_from "message_#{params[:room]}"
  end

  # def unsubscribed
  #   # Any cleanup needed when channel is unsubscribed
  # end

  # メッセージをブロードキャストするためのアクション
  # def speak(data)
  #  ActionCable.server.broadcast "message_#{params[:room]}", message: data['message']
  # end
end
  • speakの関数部分は、DBを介さずにリアルタイムでメッセージのやり取りをするときに使えます。
  • トーナメント更新のようにDB反映を元にブロードキャスト(配信)したいときはspeakは使いません。

トーナメントの勝ち上がり処理をトリガーにします

  def win
    ActiveRecord::Base.transaction do
      @tournament_bracket.win
      ActionCable.server.broadcast "message_#{@tournament_bracket.game_event_id}", message: @tournament_bracket.player_name
    end
    render json: {status: :success}
  rescue => e
    render json: {status: :error}
  end

つまり、バックエンドのアクションの後に、
ActionCable.server.broadcast "message_#{@tournament_bracket.game_event_id}", message: @tournament_bracket.player_name
を書くことで配信ができます。

  • game_event_idとは、種目のことで、該当種目のページを開いている人にのみ、ブロードキャストします。

ルーティングの設定

Rails.application.routes.draw do
  # 以下を追記
  mount ActionCable.server => '/cable'
end

フロント側

パッケージのインストール

npm install actioncable

websocketとの接続処理

.envにバックエンドでマウントしたやつを書く

BACK_WSS_URL=ws://localhost:4000/cable

utilsディレクトリ内にcable.jsを作成する

cable.jsはバックエンド側と接続するためのチャンネルを作成するものです。
また、重複してのチャンネル接続がないように、すでに接続しているチャンネルがある場合、クリアするようにしています。


import cable from 'actioncable'

let consumer

function createChannel (...args) {
  if (!consumer) {
    consumer = cable.createConsumer(process.env.BACK_WSS_URL)
  } else {
    consumer.subscriptions['subscriptions'].shift()
  }
  return consumer.subscriptions.create(...args)
}

export default createChannel

トーナメントを描画しているページで実際にチャンネルを接続する

まずは先程作ったcable.jsを読み込む

import createChannel from '~/utils/cable'

create時に実際に接続し、双方間通信状態とする

async created () {
    this.messageChannel = createChannel({channel: 'MessageChannel', room: this.gameEventId}, {
      received: (data) => {
        // ブロードキャストされたときにトーナメント情報をサーチしにいくapiを投げる
        this.searchTournamentBrackets({gameEventId: this.gameEventId})
        // 通知を表示する
        this.$notify.info({
          title: 'トーナメント更新情報',
          message: data.message
        })
      }
    })
  }

room: this.gameEventId部分が種目部分で種目のidにて接続しているので、ここで指定したid以外でのブロードキャストは受け取りません。

また通知部分に関してはelementUIを使用しています。

本番環境で使用する際は下記設定が要ります。

  • nginxの設定
location /cable {
		proxy_pass http://app/cable;
        	proxy_http_version 1.1;
        	proxy_set_header Upgrade websocket;
        	proxy_set_header Connection Upgrade;
        	proxy_set_header X-Real-IP $remote_addr;
        	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    	}
  • actioncableの設定を変える(バックエンド側)
    actioncableは指定のURL以外websocketを繋がない設定だからアクセス元の許可をしてあげます。
    config/environments/production.rb
    URLがhttps://example.comの場合
config.action_cable.url = 'wss://example.com/cable'
  config.action_cable.allowed_request_origins = [ 'https://example.com/', /https:\/\/example.*/ ]
  • websocketにはws:で始まるものとwss:で始まるものとあります。httpとhttpsの関係と同じですので、それぞれの環境に合わせる必要があります。

負荷テストしてみました

  • 同時接続数が多いときにリアルタイム更新処理をさばけるか
  • サーバーに対してどのような負荷がかかるのか

今回のテストの環境

  • 弊社staging環境を使用
  • 勤務しているメンバーに複数タブにて同じ画面を開いてもらいました!

接続数MAX 515件

image.png

負荷テスト結果は無事更新処理できました。

  • websocketでの接続と、pub/subについては、Redis(kvs)を使用しているので、メモリがやばいかなと思っていたけど、実際負荷がかかったのはCPUでした。
  • 500接続でもリアルタイム更新できました。
  • 500接続というのは、同じ種目のトーナメントを同時に見ている人(サイトへのアクセス総数ではない)

更新時のメモリ

image.png

更新時のCPU

image.png

課題

  • 弊社ではGCPを使用してるのですが、GCPにてロードバランサを設定しています。
    その際にロードバランサの外側はhttps通信をしていますが、ロードバランサ内はhttpでやりとりをしているため、リアルタイム更新ができなかった。
     →インフラの調整中です
  • トーナメントを表示したままスマホをロックした場合に更新させることができない。
     →解決できるのでしょうか??

まとめ

  • ActionCableを使用することでリアルタイム更新可能なwebアプリケーションを作成することができました。
  • 更新ボタンを押して貰う必要がなくなるため、ユーザービリティの向上につながったと思います。
  • 自分が未熟なため、リアルタイム処理の実装についてのベストプラクティスがわかりません。
  • Action cableの情報についてもチャットアプリを作って見た系はある程度。

以上です。ありがとうございました。

26
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?