Rails
websocket

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

More than 3 years have passed since last update.


  • チャット機能を実装する場合、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