#260 Messaging with Faye
- Download:
- source codeProject Files in Zip (266 KB)
- mp4Full Size H.264 Video (19.2 MB)
- m4vSmaller H.264 Video (13.5 MB)
- webmFull Size VP8 Video (36.7 MB)
- ogvFull Size Theora Video (26.5 MB)
今回のエピソードでは、Railsアプリケーションに簡単なインスタントメッセージの機能を追加します。すでにいくつかの機能を実装済みで、あるページ上のテキストフィールドにメッセージを入力できます。「送信」をクリックすると、入力されたメッセージはJavaScriptとAJAXによってチャットウィンドウに追加されます。
ここまではよさそうですが、実はこのアプリケーションには問題があります。別のチャットクライアントとして、別のウィンドウあるいはブラウザを開くと、最初のウィンドウで入力されたメッセージは別のウィンドウには表示されません。
ここで必要なのは、プッシュ通知によって他のすべてのクライアントに新規メッセージが追加されたことを知らせて表示させることです。この機能を実装する方法はいくつかありますが、その前に今までにできているコードを見てみます。これはAJAXとjQueryを使用した簡単なフォームです。特に複雑な点はありませんが、もしjQueryがよくわからない場合はこれ以降読み進める前にエピソード136[動画を見る, 読む]を参照することをお勧めします。
このアプリケーションではjQueryを使用するため、まず最初にGemfile
にjquery-rails gemを追加します。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'jquery-rails'
次にチャットページのビューのコードを見てみます。メッセージを一覧表示する、chat
のid
のリストと、:remote => true
を使用してAJAXでメッセージを送信するフォームがあります。
<% title "Chat" %> <ul id="chat"> <%= render @messages %> </ul> <%= form_for Message.new, :remote => true do |f| %> <%= f.text_field :content %> <%= f.submit "Send" %> <% end %>
フォームはMessagesController
のcreate
アクションに送信されます。
class MessagesController < ApplicationController def index @messages = Message.all end def create @message = Message.create!(params[:message]) end end
create
アクションにはJavaScriptのテンプレートがあり、 新しいメッセージをリストに追加してフォームをリセットしています。
$("#chat").append("<%= escape_javascript render(@message) %>");
$("#new_message")[0].reset();
これはごく標準的なJavaScriptのコードです。ここで上のコードの1行目を書き換えして、新規メッセージがすべてのクライアントに送信されるように変更します。
これをどう実現すればいいでしょうか? 実はRailsは非同期イベントの処理はあまり得意ではありません。それは、Railsアプリケーションに対してソケットを開いたままにできないからです。ひとつの対応策として、この種の問題を処理するのに適した別のWeb開発フレームワークの採用を検討できるかも知れません。Node.jsのようなフレームワークでSocket.IOを使用するか、あるいはRubyにこだわりたければ、Crampやasync_sinatra、または新しいフレームワークであるGoliathなどの優れた解決方法があります。しかしRailsにこだわるとしたらどうでしょう? アプリケーションの開発にRailsを使い続けながら、配信と購読の仕組みを使って非同期イベント処理を扱うことができればいいのですが。
そこでFayeの登場です。Fayeは非同期に配信と購読を処理するサーバです。Railsアプリケーションと一緒に使用でき、その特別な機能が必要なときに呼び出すことができます。Fayeには2つのタイプがあります。Node.jsサーバとRubyサーバです。どちらも同じプロトコルを使用するので、我々がより慣れている言語用の方を選択できます。言うまでもなく、ここではRubyサーバの方を選択します。
Fayeを使えるようにするために、まずFaye gemをインストールします。
$ gem install faye
次にRailsアプリケーションのappディレクトリの直下にfaye.ru
というRackupファイルを作成します。そのファイルでは、Fayeのドキュメントにある1行のコードをそのまま貼付けて新規Rackアプリケーションを作成し、続いてそれを実行します。
require 'faye' faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45) run faye_server
rackup
コマンドを実行してサーバを起動します。これはThinサーバでproductionモードで動くように設定されているので、2つのオプションを指定します。
$ rackup faye.ru -s thin -E production >> Thin web server (v1.2.11 codename Bat-Shit Crazy) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:9292, CTRL+C to stop
Fayeアプリケーションが9292ポートで起動しました。サーバにはJavaScriptファイルがあり、レイアウトファイルでincludeします。このファイルはhttp://localhost:9292/faye.js
にあり、上で設定した:mount
オプションに基づいて名前がついています。
<%= javascript_include_tag :defaults, "http://localhost:9292/faye.js" %>
もちろんproductionモードでは、正しいサーバを指定するようURLを変更する必要があります。
FayeのサイトのDocumentationにブラウザからの利用方法がありますが、それによるとFayeのJavaScriptをincludeしたら新規クライアントを作成します。そのためにapplication.jsファイルにコードを追加します。
/public/javascripts/application.js
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); });
$
関数を使って、ページのDOMがロードされるまでコードが実行されないようにしています。このファイルについても、productionモードではURLを変更する必要があります。
Fayeクライアントが設定できたら、チャンネルを購読できるようになります。作成中のアプリケーションには1ページしかないので、チャンネルは1つあればいいでしょう。それを/messages/new
とします。チャンネルを購読するには、subscribe
関数を呼び出してチャンネル名とコールバック関数を渡します。メッセージを受信するとコールバック関数が起動され、データが渡されます。今のところは、返されたデータを見るためにalert
するだけにします。
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); faye.subscribe('/messages/new', function (data) { alert(data); }); });
では試してみましょう。作成中のRailsアプリケーションを起動してチャットページを開きます。JavaScriptが読み込まれ、Fayeクライアントがメッセージの受信状態になります。curl
を使ってコールバックを手動で起動して、チャンネルにメッセージを送ります。
$ curl http://localhost:9292/faye -d 'message={"channel":"/messages/new", "data":"hello"}'
POSTデータは正しく動作させるために、メッセージパラメータとJSONデータという形式で送信します。JSONにはchannelとdataというキーを持たせます。
curl
コマンドを実行すると、すぐにブラウザにアラートメッセージが出て、送信したデータが表示されます。
これでFayeにPOSTリクエストを送信することでRailsアプリケーションを介して通知を送信できるようになりました。
メッセージを配信する
次にcreate.js.erb
ファイルを編集して、メッセージが送信されたらFayeを介してすべての購読しているブラウザにメッセージが配信されるように修正します。例えば、channel
パラメータとブロックを持つbroadcast
というメソッドがあれば便利でしょう。ブロックに渡されるものはすべてチャンネルに対して配信されます。
このメソッドをApplicationHelper
に作ってみましょう。そこで、channel
パラメータとブロックの出力からメッセージを組み立て、Net::HTTP.post_form
を使ってデータをFayeサーバにPOSTします。
module ApplicationHelper def broadcast(channel, &block) message = {:channel => channel, :data => capture(&block)} uri = URI.parse("http://localhost:9292/faye") Net::HTTP.post_form(uri, :message => message.to_json) end end
Net::HTTP
を使いますが、これはデフォルトではRailsに含まれていないためrequire
する必要があります。これを/config/application.rb
ファイルに記述します。
require File.expand_path('../boot', __FILE__) require 'rails/all' require 'net/http' # rest of file...
新しいbroadcastメソッドができたのでcreate.js.erb
で使用します。
<% broadcast "/messages/new" do %> $("#chat").append("<%= escape_javascript render(@message) %>"); <% end %> $("#new_message")[0].reset();
ここで動作を試してみましょう。アプリケーションに戻ってチャットメッセージを入力すると、Fayeを介して配信され、新しいメッセージをリストに追加するためのJavaScriptを確認することができます。
ブラウザがJavaScriptを表示するのではなく評価させるために、alert
をeval
に変更します。
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); alert('subscribing!') faye.subscribe('/messages/new', function (data) { eval(data); }); });
ページを再度読み込み、別のウィンドウを開いてこの機能を試します。チャットウィンドウにメッセージを入力すると、すぐに別のウィンドウにも表示されます。
これで設定ができたので、シンプルなbroadcast
ブロックを用いて、AJAXリクエストを受け付けて購読しているすべてのクライアントにJavaScriptを配信できるようになりました。各クライアントでJavaScriptを実行する代わりにJSONを扱いたければ、同じような方法でJavaScriptの代わりにJSONを返して評価されるようにもできます。
セキュリティ
ここまでで書いたコードはうまく動きましたが、安全ではありません。前半でコマンドラインからcurl
を呼び出してメッセージを配信しましたが、今のコードのままでは誰でも同じようにJavaScriptを送信して、あるチャンネルを聞いているすべてのクライアントで評価されてしまいます。
この問題はFayeの拡張機能を利用することで解決できます。Documentationにも説明がありますが、ここで簡単に触れておきます。それらを使うにはRubyクラスを作成し、incoming
あるいはoutgoing
メソッドを追加します。作成したメソッドが認証トークンを読み、それが期待したものでなかったらエラーを返します。それからRackupファイルで、Fayeのadd_extension
メソッドを使ってFayeサーバの機能拡張としてクラスを追加します。
FayeサーバとRailsアプリケーションで共有するトークンを作成し、毎回メッセージを受けつける前にトークンが一致するかをチェックします。このためにRailsアプリケーションにfaye_token.rb
という初期設定ファイルを追加します。このアプリケーションを利用するときに、各システムでこの値がユニークである必要があるため、このファイルをGitリポジトリには含んでいないことに注意してください。このファイルで、どのような値も持つことができるFAYE_TOKEN
という定数を設定します。
FAYE_TOKEN = "anything_here"
次にbroadcast
メソッドを書き換えて、このトークンと送信メッセージを含むように修正します。拡張機能の情報を:ext
パラメータに含むようにして、そのパラメータ内で:auth_token
という名前でトークンを送信します。
module ApplicationHelper def broadcast(channel, &block) message = {:channel => channel, :data => capture(&block), :ext => {:auth_token => FAYE_TOKEN}} uri = URI.parse("http://localhost:9292/faye") Net::HTTP.post_form(uri, :message => message.to_json) end end
最後にfaye.ru
ファイルを修正して認証を処理する拡張機能を追加します。
require 'faye' require File.expand_path('../config/initializers/faye_token.rb', __FILE__) class ServerAuth def incoming(message, callback) if message['channel'] !~ %r{^/meta/} if message['ext']['auth_token'] != FAYE_TOKEN message['error'] = 'Invalid authentication token.' end end callback.call(message) end end faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45) faye_server.add_extension(ServerAuth.new) run faye_server
前に作成した初期設定ファイルからFayeトークンを読み込み、incomingメソッドを持つServerAuth
という新規クラスを作成します。このメソッドでまずチャンネル名の初めに「meta」がついていないことを確認します。「meta」はFayeが内部的に使用する名前につくもので、そのようなチャンネルは認証対象ではないからです。次にauth_token
が正しく、エラーメッセージがないことを確認します。そしてコールバックを呼び出します。これにより、エラーが含まれるメッセージを受け付けないようにします。最後にファイルの最下部で、Fayeサーバを生成した直後に拡張機能メソッドを追加しています。
両方のサーバを再起動してcurl
コマンドを送信すると、認証された要求ではないためBad Request responseが返されます。
$ curl http://localhost:9292/faye -d 'message={"channel":"/messages/new", "data":"hello"}' HTTP/1.1 400 Bad Request Content-Type: application/json Connection: close Server: thin 1.2.11 codename Bat-Shit Crazy Content-Length: 11
一方、Railsアプリケーションは正しい認証トークンを送信するので、正常に動作します。
Fayeの利用方法についての今回のエピソードはここまでです。Fayeは、Webフレームワークを完全に入れ替えることなくプッシュ通知機能を取り扱えるようにする優れた方法です。Railsのロジックはすべてそのままで、プッシュ通知の利点を享受できます。
自分でFayeサーバを管理したくないという場合は、Pusherというサーバがありイベント処理を代行してくれるので一度見てみてはいかがでしょうか?