※この記事はRails4までの旧来のやり方でWebsocket通信を実装しています。Rails5からはAction Cableが導入されてWebsocket通信の実装は、より容易になりました。

Websocket通信って?

Webでの情報のやり取りはHTTP通信が基本。
しかしHTTP通信では、サーバーに対してリクエスト(例えばページの更新)を送らない限りクライアントが持っている情報が更新されることはない。
いちいちユーザーが「ページを更新するのが面倒だけど自動で情報は更新してほしいな」ってことで使われ始めたのがAjax通信。ただこれでは、裏でリクエストを送ってJavaScriptで書き換えているだけ。リアルタイムなやり取りをするには常に裏でやり取りをしなければならない。
そんなわけで使われ始めたのがWebsocketである。これは必要に応じてサーバーからクライアントに対して情報を送ってくれるシステムで、情報を得るためにクライアントがリクエストする必要がないってところがミソ。

とりあえずやってみる

ネタとしてはベタだけどチャットを作ってみる。

使うGem

  • rails newした時にインストールされるgem
  • websocket-rails (今回の主役)

Gemfileのどこかにgem 'websocket-rails'を加えておく。

Gemfile
gem 'websocket-rails'

bundleの実行も忘れずに・・・
そしてbundleが終わったら重要な作業をする必要がある。
Gemfile.lockを眺めていると次のようなコードが見つかるはずである。(2016/02/17現在)

Gemfile.lock
#(前略)
faye-websocket (0.10.2)
  eventmachine (>= 0.12.0)
  websocket-driver (>= 0.5.1)
#(後略)

ところがfaye-websocketのバージョン0.10.2では動かない。
そういうわけでバージョンを0.10.0に下げて再度bundleを行うこと。

Gemfile.lock
#(前略)
faye-websocket (0.10.0)
  eventmachine (>= 0.12.0)
  websocket-driver (>= 0.5.1)
#(後略)

筆者も、これに気付かず前書いたコードと同じはずなのに動かないと、1週間近く悩んだ。
そしたらrailsにwebsocket_railsをインストールする。

ターミナル
rails g websocket_rails:install

実行が完了したらconfig/events.rbなるファイルが作られているはず。
今度はconfig/environments/development.rbにconfig.middleware.delete Rack::Lockを書き加える。
ブロック内ならどこに加えても構わないが分かりやすくendの上に加えておく。

config/environments/development.rb
Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded on
  # every request. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  (中略)

  # Raises error for missing translations
  # config.action_view.raise_on_missing_translations = true

  config.middleware.delete Rack::Lock #このコードを加える
end

これで下準備完了!

ルーティング

前の工程で出来上がったconfig/events.rbはWebsocket用のルーティングファイルといったところ。
コメントが多いので、全部消して次のように書き換える。

config/events.rb
WebsocketRails::EventMap.describe do
  subscribe :send_message, 'messages#new'
end

:send_messageがイベント名、'messages#new'が実行されるコントローラとアクションである。

:send_messageなんてヤダ、ネストして書きたい!」なんて時はnamespaceを使う。

config/events.rb
WebsocketRails::EventMap.describe do
  namespace :messages do
    subscribe :send, 'messages#new'
  end
end

細かいアクセス方法は後々・・・

コントローラ

前の工程でmessagesというコントローラを呼び出しているが、まだ作っていない。
これをapp/controllersの中に作成する。このフォルダの中にmessages_controller.rbと言うファイルを作成する。ファイル命名規則はrailsのルールと同じ。
そしたら中身を次のようにする。

app/controllers/messages_controller.rb
class MessagesController < WebsocketRails::BaseController
  def new
    data = { msg: 'msg recieved.' }
    send_message :spread_message, data
  end
end

名前の通りsend_messageはメッセージをクライアントに送るメソッド。:spread_messageはイベント名(こちらはルーティング不要)、dataは送るデータである。send_messageは、このようにハッシュでデータのやり取りをする。

ビュー

ここまでサーバサイドをいじってきたが、いよいよクライアントサイドを作っていく。とりあえず実験なのでapplicationコントローラの中にhomeアクションを作ることにする。
app/views/application/home.html.erbを作成して中身を次のようにする。

app/views/application/home.html.erb
<form  id="form">
  <input type="submit">
</form>

<script>
  var dispatcher = new WebSocketRails("localhost:3000/websocket"); //ドメインは適宜変える
  var form = document.getElementById("form");

  form.onsubmit = function(e){
    dispatcher.trigger("send_message");
    e.preventDefault();
  }
  dispatcher.bind("spread_message", function(data) {
    console.log(data.msg);
  });
</script>

dispatcher.trigger("send_message");によってsend_messageイベントがトリガされてapp/controllers/messages_controller.rbのMessagesクラスnewメソッドが実行される。
dispatcher.bind("spread_message", ~)は、spread_messageイベントを監視していて、そこで何かを受け取った場合に実行される。
これでsend_messagesに対してWebsocket通信を行って、spread_messageからレスポンスが返ってくる構造が完成する。

namespaceを使ってWebsocketのルーティングをした場合のイベント名は(namespace).~である。

config/events.rb
WebsocketRails::EventMap.describe do
  namespace :messages do
    subscribe :send, 'messages#new'
  end
end

この場合dispatcher.trigger('messages.send');とすれば良い。

ルーティングもしっかりしておく。

config/routes.rb
Rails.application.routes.draw do
  root to:'application#home'
end

ここまで出来たら送信ボタンを押したらコンソールにmessage recieved.と出ているのが確認できるだろう。

おまけ

処理が成功したか失敗したかで、ブラウザでの処理を変えたい時に便利なメソッド。
trigger(send_add, data, success, failture)
send_add: 送り先
data: 送りたいデータ
sucess: 成功の時に実行したい処理(関数)
failture: 失敗の時に実行したい処理(関数)

サーバ側で成功した時にtrigger_success dataを実行すると、successが
trigger_failture dataを実行するとfailtureが実行される。

ここから本番

前章で使い方を簡単にまとめたので、ここからチャットへと仕上げていく。

コントローラ

app/controllers/messages_controller.rb
class MessagesController < WebsocketRails::BaseController
  def new
    send_message :spread_message, message #ブラウザから送られてきたデータはmessageに入っている
  end
end

ビュー

app/views/application/home.html.erb
<form  id="form">
  <input id="text_input">
  <input type="submit">
</form>
<ul id="msg_box">
</ul>
<script>
  var dispatcher = new WebSocketRails("localhost:3000/websocket"); //ドメインは適宜変える
  var form = document.getElementById("form");
  var text_input = document.getElementById("text_input");
  var msg_box = document.getElementById("msg_box");

  form.onsubmit = function(e){
    dispatcher.trigger("send_message", { msg: text_input.value });
    e.preventDefault();
  }
  dispatcher.bind("spread_message", function(data) {
    var li = document.createElement("li");
    li.textContent = data.msg;
    msg_box.appendChild(li);
  });
</script>

これでチャットアプリの完成!
ではない。タブを複数開くとわかると思うが、データを送ったところにしかデータが返ってきていない。
そうsend_messageでは不十分なのである。
複数の人に送らなければならない時は、broadcast_messageを使う。

app/controllers/messages_controller.rb
class MessagesController < WebsocketRails::BaseController
  def new
    broadcast_message :spread_message, message
  end
end

これで本当にチャットの完成である。

終わりに

今回紹介した、broadcast_messagesend_messageはrailsのコントローラで使うことができない。
またWebsocket-railsには、複数配信用にチャンネルを使うことができる。これは後々紹介したいと思う。

参考文献