LoginSignup
158

More than 5 years have passed since last update.

Railsでチャット機能を実装する方法(実装編)

Posted at
  • チャット機能を実装する場合、WebSocketを使う、Server Sent Event(SSE)を使う、Pusherなどのサービスを活用するなど、いくつかの方法がありますが、私のケースでは、WebSocketを使うのが最もマッチしていたので、WebSocketを利用することを考えます。この辺の選定についてはまた別途まとめたいと思います
  • RailsでWebSocketを使う場合、websocket-railsを使うのが、手っ取り早くて良いのではないかと思います
  • そこで、websocket-railsでの、チャネル、プライベートチャネル、セキュリティ、認証あたりの関係性が分かりにくかったので、自分なりに整理してみました

websocket-railsの基本的な使い方

Event Routerへのイベントの追加

  • JSクライアントからのリクエストをRailsのコントローラーとマッピングしている
event.rb
WebsocketRails::EventMap.describe do
  namespace :tasks do
    subscribe :create, :to => TaskController, :with_method => :create
  end
end

JSクライアントからイベントをトリガー

var dispatcher = new WebSocketRails('localhost:3000/websocket');
dispatcher.trigger('tasks.create', {name: "hogehoge"});

イベントのハンドリング

  • send_messageで、クライアント側にプッシュできる
    • send_messageは、このイベントを実行したクライアントにしか送信しない
    • クライアントが接続した際に"接続完了しました"のようなメッセージを表示する場合に使える
  • 一方、broadcast_messageは、接続している全てのクライアントにプッシュできる
class TaskController < WebsocketRails::BaseController
  def create
    task = Task.new message
    if task.save
      send_message :create_success, task, :namespace => :tasks
    else
      send_message :create_fail, task, :namespace => :tasks
    end
  end
end

JSクライアントでレスポンスを受け取る

  • dispatcher.bindで、コールバックを登録しておく
dispatcher.bind('tasks.create_success', function(task) {
  console.log('successfully created ' + task.name);
});

チャネルを使用する場合

  • チャネルは、WebSocket接続したクライアントに対して任意のタイミングでメッセージを送ることができる
  • チャネルは、複数のチャットルームを管理して、特定のクライアントにブロードキャストする必要がある場合に使える
    • チャネルがない場合には、イベントを指定して、そのイベントをトリガーしたクライアントにプッシュする(send_message)か、WebScoketに接続している全クライアントにブロードキャストする(broadcast_message)するかしかない

Event Routerへのイベントの追加

WebsocketRails::EventMap.describe do
  subscribe :new_post, to: ChatController, with_method: :create_post
end

JSクライアントでチャネルをサブスクライブする

  • dispatcher.subscribeでチャネルをサブスクライブすることができる
    • チャネル名は、乱数などで動的に指定することで、そのチャネル名を知る特定のクライアントだけがアクセスできるチャネルをつくることができる
var dispatcher = new WebSocketRails('localhost:3000/websocket');
postsChannel = dispatcher.subscribe('posts');

JSクライアントからイベントをトリガー

  • dispatcher.triggerでイベントをトリガーできる(ここは、チャネル名ではない点に注意)
dispatcher.trigger('new_post', post);

イベントのハンドリング

  • WebsocketRails[:posts].triggerとチャネル名を指定して、triggerをすることで、このチャネルにメッセージをブロードキャストできる
class ChatController < WebsocketRails::BaseController

  def create_post
    post = Post.new(message)
    post.cook

    if post.save
      WebsocketRails[:posts].trigger 'new_post', post
    else
      puts "Failed to saved post"
    end
  end

JSクライアントでレスポンスを受け取る

  • dispatcherではなく、チャネル.bindでコールバックを登録しておく
postsChannel.bind('new_post', function(post) {
  /* イベント受け取り後の処理 */
});

Working with Channels · websocket-rails/websocket-rails Wiki · GitHub

プライベートチャネル

  • プライベートチャネルは、1クライアントからの接続を保証するもの
    • 接続した際に、他のクライアントが接続していれば、全部追い出して、そのクライアントだけを接続するようにする
    • チャットのような2人のクライアントが接続するようなアプリケーションには向いていない
    • このようなアプリケーションでプライベートチャネルを使いたい場合は、2つのプライベートチャネルを作って、それぞれにメッセージを送り合うようなことをする必要がある
    • もしくは、チャネル名をユニークにすれば、そのユニークなチャネルにアクセスした限定されたクライアントだけがアクセスできる状況を作ることができる
  • セキュアとは異なる概念
    • 当たり前だけどプライベートとは、他の誰かが接続していないというだけで、その接続がセキュアというわけではない
    • また、チャネル名をユニークにしても、そのチャネル名がわかれば接続できてしまう
  • プライベートチャネルはパブリックから、プライベートに変更することができる
    • デフォルトでは、パブリックチャネルに接続していたクライアントは、そのチャネルから追い出されてしまうが、以下の設定をすることでプライベートチャネルでも複数クライアントからの接続を維持することができる
config.keep_subscribers_when_private = true

Using Private Channels · websocket-rails/websocket-rails Wiki

How to broadcast data to specific subcsriber? · Issue #104 · websocket-rails/websocket-rails

How to prevent spam when using websocket-rails gem? - Stack Overflow

Security

  • window.location.protocolhttpsであれば、セキュアなプロトコルwssで通信してくれる

WebSocket Security | Heroku Dev Center

websocket-rails: standaloneモードのサーバでSSL通信 - Qiita

UserManager

  • UserMangerを使えば、特定のユーザーにメッセージを送ることができる
event.rb
subscribe :client_connected to: WebsocketController, with_method: :client_connected
  • wss://localhost:3000/websocket?client_id=some_unique_idでリクエストすれば、client_connectedがコールされて、client_idをもつuserが登録される
class WebsocketController < WebsocketRails::BaseController

  def client_connected
    WebsocketRails.users[params[:client_id]] = connection
  end

end

Access one specific client from anywhere (regular controller / delayed task) · Issue #164 · websocket-rails/websocket-rails

Websocket Railsを使って特定のユーザーにメッセージを送る - 亀の速度で走る

current_user

  • ApplicationControllerで、current_userというメソッドが定義されていて、ユーザーがサインインし、かつWebSocketコネクションが確立されている場合、そのコネクションは、UserManagerに格納される
    • 逆に、current_userが定義されていない、もしくはサインインしていない場合は、UserManagerにコネクションは格納されない
  • つまり、Deviseを使用していれば、自動的にUserManager.usersが手に入る
current_user以外
  • ただし、DeviseでUser以外のモデルを使用している場合(例えばAdmin)、current_userではなく、current_adminとなる
    • current_adminを使用したい場合は、user_classを変更することで対応できるとあるが、自分の場合は、User以外うまく動かなかった
config.user_identifier = :id
config.user_class = Admin
  • このプルリクにあるように、StandaloneモードでNameError: uninitialized constantが表示される場合は、procを使うと良いとある
    • でも、これをやってもUserManager.usersは空のまま。。。

Recommend overriding user_class with a proc · alexdunae/websocket-rails@3612137

認証/認可

  • WebSocketのプロトコル自体は、認証や認可の仕組みは提供していないので、アプリケーションで実装する必要がある
  • WebsocketRails::BaseControllerを継承したコントローラー内で、ApplicationControllerのメソッドは普通に使えるので、Deviseを使っている場合は、current_userが使える
  • プライベートチャネルと組み合わせて、サーバー側で認証する方法が紹介されている

Using Private Channels · websocket-rails/websocket-rails Wiki

Solves Warden compatibility and gives the possibility to register an user after connection established. by phlegx · Pull Request #217 · websocket-rails/websocket-rails

current_user is not compatible with Warden (Devise) · Issue #216 · websocket-rails/websocket-rails

Can't Access Helpers in Websocket Controller · Issue #3 · websocket-rails/websocket-rails

Register and Authenticate User · Issue #184 · websocket-rails/websocket-rails

websocket-rails gem and authentication - Stack Overflow

Filtering Channel Events · websocket-rails/websocket-rails Wiki

Standalone Mode

  • スタンドアロンモードでは、Redisを起動して、イベント処理を任せることができる
WebsocketRails.setup do |config|
  config.standalone = true
end
  • 以下のコマンドで、スタンドアロンサーバーを起動、停止できる
    • Redisを起動した上で、実行する
$rake websocket_rails:start_server
$rake websocket_rails:stop_server

Standalone Server Mode · websocket-rails/websocket-rails Wiki · GitHub

Onlineの検出

How do I tell if a user is online? | Ryan Epp

ActionCable Devise Authentication

Unable to reach channel subscribers from SideKiq workers · Issue #173 · websocket-rails/websocket-rails

Synchronization

Multiple Servers and Background Jobs · websocket-rails/websocket-rails Wiki · GitHub

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
158