CTC 教育サービス
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes
5/6にRails3.2.18、4.0.5および4.1.1がリリースされました(*1)。このリリースはディレクトリトラバーサル脆弱性に対するセキュリティフィックスですので、できるだけ早くアップデートすることをお勧めいたします。
さて前回までサーバプッシュを行うための技術として、WebSocketをご紹介してまいりました。今回はその番外編として、Rails4から導入されたActionController::Liveによるサーバプッシュについて書いていきたいと思います。
なお、動作環境は以下の通りです(*2)。
第12回「WebSocketでサーバプッシュ」の中でもお伝えしましたが、これまでのWebの仕組みではサーバプッシュを行うことができませんでした。このサーバプッシュを行うための技術として、2つの仕組みが規格化されることとなります。
1つは前回までご紹介したWebSocketです。WebSocketはHTTPではない独自のプロトコル(WebSocketプロトコル)を用いるという方法でサーバプッシュを実現しました。
これに対し、HTTPのみでサーバプッシュを実現する方法も規格化されます。それが「Server-Sent Events(以下、SSE)」です(*3)。
SSEはCometを標準化したような技術で、以下のような特徴を持ちます。
HTTP上で動作するため、サーバを含めた既存のWeb環境で動作することは大きなポイントです(*4)。
反面注意点としては、HTTPの制約を受けることや、SSEのコネクションを用いてクライアントからリクエストが送れない(*5)ことなどが挙げられます。
ActionController::LiveはSSEにおけるサーバサイドを実装するAPIとして、Railsに組み込まれています。
それでは、実際に使ってみましょう。今回も芸なく前回までと同様にチャット機能を実装してみたいと思います。
作成の流れは以下の通りです。
なお、ActionCotroller::LiveそのものはRailsに標準で組み込まれていますので、Gemなどによるインストールの必要はありません。
1. コントローラの実装
まずはコントローラを実装します。ジェネレータを使い、コントローラと画面表示用のビューを合わせて生成します。
> rails g controller chat index create app/controllers/chat_controller.rb route get 'chat/index' invoke erb create app/views/chat create app/views/chat/index.html.erb (略)
生成後、「app/controllers/chat_controller.rb」を以下のように編集します。
class ChatController < ApplicationController # ActionController::Liveをinclude include ActionController::Live # 接続中のクライアント一覧を格納する配列 @@streams ||= [] # チャット画面表示アクション def index end # SSE接続用アクション def stream response.headers['Content-Type'] = 'text/event-stream' @@streams.push(response.stream) # 20秒おきにクライアントへダミー送信 loop do response.stream.write(":ping ¥n¥n") sleep 15 end rescue IOError ensure @@streams.delete(response.stream) response.stream.close end # コメントリクエスト受付アクション def message @@streams.each do |stream| stream.write("data: #{params[:comment]}¥n¥n") rescue nil end render text: nil end end
ここでのポイントを順に説明していきます。まずはstreamアクションについてです。このアクションはSSE用のコネクションを確立・保持するためのものです。
[Line 15]
response.headers['Content-Type'] = 'text/event-stream'
SSEの仕様では、レスポンスのMIMEタイプを「text/event-stream」にしなければならないと定められています。ここではレスポンスヘッダのContent-Typeに「text/event-stream」を設定しています。 なお後述のwriteメソッドやcloseメソッドが呼ばれる前にレスポンスヘッダを指定しないと、エラーになるので注意してください。
[Line19~22]
loop do response.stream.write(":ping ¥n¥n") sleep 15 end
response.stream(*6)に対してwriteメソッドを呼ぶことで、クライアントにデータを送ることができます。送信データは、例えば以下のような形式になります(*7)。
送信データは各行「(要素名): (値)」の形となり、また空行の直前までを一つの送信単位とします。この例では1行目のデータ、3行目+4行目のデータ、6行目のデータという単位で3回クライアントに送信されます(*8)。
また、指定できる要素名は以下の通りです。
要素名 | 説明 |
data | 送信するデータ(本文)を設定する。 |
event | イベント名を設定する。 設定した場合、クライアントではこのイベント名でイベントが発火する。 |
id | イベントIDを設定する。 設定した場合、クライアントが再接続を行う際にLast-Event-IDとしてこの値がリクエストヘッダに付与される。 |
retry | クライアントが再接続を行う間隔をミリ秒で設定する。 |
上記以外の要素名は、全て無視されます。また、先頭に「:(コロン)」を付与するとコメント行と解釈されます。
このソースコードでは15秒ごとにコメントの送信を行い、コネクションが切断しないようにしています。
[Line 27]
response.stream.close
response.streamに対してcloseメソッドを呼ぶことで、コネクションを切断します。この処理は当該アクション内で必ず行う必要があります(*9)。このソースコードではIOError発生時(*10)、loopメソッドを抜けた後にensure節で実行されるようにしています。
続いてmessageアクションについてです。前述の通りSSEにて確立したコネクションを使って、クライアントからサーバへデータを送信することができません。そこでクライアントからのデータ送信は別途Ajaxリクエストで行うこととします。
[Line32~34]
@@streams.each do |stream| stream.write("data: #{params[:comment]}¥n¥n") rescue nil end
messageアクションではリクエストを受け、SSEにて接続しているクライアント全てにリクエストパラメータとして飛んできたデータを送信しています(*11)。
2. routes.rbの設定
続いてルーティングを定義します。「config/routes.rb」を以下のように編集します。
Rails.application.routes.draw do get 'chat/index' # チャット画面表示 get 'chat/stream' # SSE接続 post 'chat/message' # コメント受付 end
3. クライアントサイドの設定
次はクライアントサイドの実装です。SSEをクライアントで扱うには、EventSourceと呼ばれるオブジェクトを使用します。以下にそのメソッドなどの一覧を示します。
コンストラクタ | |
new EventSource(url) | EventSourceオブジェクトを生成し、SSEによる接続を開始する。 [url] 接続先のURLを指定 |
メソッド | |
close() | SSEによる接続を終了する。 |
イベント | |
open | SSE接続が正常に確立したときに発火する。 |
error | SSE接続においてエラーが発生したときに発火する。 |
message | 接続先よりサーバプッシュされたときに発火する。 |
なおイベントに関しては前述の通り、サーバ側でeventが指定された場合にはそのイベント名でも発火します。
「app/views/chat/index.html.erb」を以下のように編集します。
<!-- チャット表示部分 --> <ul id="chat_area"> </ul> <!-- コメントフォーム --> <%# Ajaxリクエストで送信 %> <%= form_tag "/chat/message", :remote => true do %> <%= text_field_tag :comment %> <%= submit_tag "send" %> <% end %> <script> // SSEの接続 var sse = new EventSource("/chat/stream"); // メッセージ受信時の処理 sse.onmessage = function(event) { var message_li = document.createElement("li"); message_li.textContent = event.data; document.getElementById("chat_area").appendChild(message_li); }; </script>
SSEによる接続の開始処理の後、messageイベントの購読を行っています。再接続処理を書いていませんが、前述の通りコネクションが切断した場合はEventSourceが自動で再接続を行ってくれます(*12)。
4. development.rbの編集
Websocket-Railsと同様にdevelopmentモードでActionController::Liveを使用する際はRack::Lockを無効にする必要があります。また、合わせてクラスのeager_loadをtrueに設定しておきましょう(*13)。
「config/enviroments/development.rb」を以下のように編集します。
Rails.application.configure do (略) # eager_loadをtrueに変更 config.eager_load = true (略) # 以下を追加 config.middleware.delete ::Rack::Lock end
5. pumaのインストール
さてサーバを起動して動作確認といきたいところですが、デフォルトで起動するWEBrickではActionController::Liveを動作させることができません。そこでここではpuma(*14)をインストールします。
以下をGemfileに以下を追記し、「bundle install」を実行します。
gem "puma"
インストールが完了したら「rails s」でサーバを起動し、「http://localhost:3000/chat/index」にアクセスしてみましょう。WebSocketの時と同様の動きをすることが確認できるかと思います。
またChromeの開発者ツールにてNetworkタブを見てみると、「stream」のコネクションが切断されずにいるのを確認することができます。
ここまでActionController::Liveについてご紹介してまいりました。SSEとWebSocketでは、正直WebSocketの方が使い勝手のいい印象です。ですが、状況によっては選択できないケースもあると思いますので、そのような際は是非ActionController::Liveを検討してみてください。
それでは、Enjoy Ruby!
*1 : http://weblog.rubyonrails.org/2014/5/6/Rails_3_2_18_4_0_5_and_4_1_1_have_been_released/
*2 : Mac環境(Mac OS X 10.9.3)でも動作確認をしています。
*3 : http://www.w3.org/TR/eventsource/
*4 : しかしながら執筆時点ではIEがSSEをサポートしていません。
*5 : HTTPのコネクションを張り続けているだけなので、当たり前といえば当たり前ですが。
*6 : 戻り値はActionController::Live::Bufferオブジェクトです。
*7 : 注釈*3のURLより抜粋
*8 : クライアントではmessageイベントが3回発火します。
*9 : 仮に当該アクション内でエラーが発生しても、です。ActionController::Liveのdocにはcloseメソッドを呼ばない場合、「the socket may be left open forever」であると書かれています。そのため、ensure節の中に書いておくのが一般的なようです。
*10 : ブラウザを閉じるなど、切断されたクライアントに対してwriteメソッドでデータ送信を試みると、IOErrorが発生します。
*11 : クライアントへのデータ送信はSSEのコネクションにて行っています。そのため、このアクション自体のレスポンスボディは空(render text: nil)にしています。
*12 : なお、コネクションを切断したい場合には意図的にcloseメソッドを呼ぶ必要があります。
*13 : eager_loadをtrueにすることで、アプリケーションをスレッドセーフにすることができます(Rails4以降)。
*14 : https://github.com/puma/puma
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes