IT・技術研修ならCTC教育サービス

サイト内検索 企業情報 サイトマップ

研修コース検索

コラム

Ruby & Rails

CTC 教育サービス

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes 

第15回 サーバプッシュ番外編 ~ActionController::Live~ (松永紘) 2014年6月

 5/6にRails3.2.18、4.0.5および4.1.1がリリースされました(*1)。このリリースはディレクトリトラバーサル脆弱性に対するセキュリティフィックスですので、できるだけ早くアップデートすることをお勧めいたします。

 さて前回までサーバプッシュを行うための技術として、WebSocketをご紹介してまいりました。今回はその番外編として、Rails4から導入されたActionController::Liveによるサーバプッシュについて書いていきたいと思います。

 なお、動作環境は以下の通りです(*2)。

  • Windows 7
  • Chrome 35
  • Ruby 2.1.2
  • Rails 4.1.1
Server-Sent Events
 

 第12回「WebSocketでサーバプッシュ」の中でもお伝えしましたが、これまでのWebの仕組みではサーバプッシュを行うことができませんでした。このサーバプッシュを行うための技術として、2つの仕組みが規格化されることとなります。

 1つは前回までご紹介したWebSocketです。WebSocketはHTTPではない独自のプロトコル(WebSocketプロトコル)を用いるという方法でサーバプッシュを実現しました。

 これに対し、HTTPのみでサーバプッシュを実現する方法も規格化されます。それが「Server-Sent Events(以下、SSE)」です(*3)。

 SSEはCometを標準化したような技術で、以下のような特徴を持ちます。

  • HTTP上で動作
  • 必要な分だけコネクションを維持し、イベントに応じて都度データを送信
     (サーバがレスポンスヘッダに「Transfer-Encoding: chunked」を指定すると、クライアントでは送られたデータを都度処理する)
  • クライアントサイドでは、コネクションが切断されると自動で再接続

fig01

 HTTP上で動作するため、サーバを含めた既存のWeb環境で動作することは大きなポイントです(*4)

 反面注意点としては、HTTPの制約を受けることや、SSEのコネクションを用いてクライアントからリクエストが送れない(*5)ことなどが挙げられます。

 ActionController::LiveはSSEにおけるサーバサイドを実装するAPIとして、Railsに組み込まれています。

チャット機能の実装

 それでは、実際に使ってみましょう。今回も芸なく前回までと同様にチャット機能を実装してみたいと思います。

 作成の流れは以下の通りです。

  1. コントローラの実装
  2. routes.rbの設定
  3. クライアントサイドの実装
  4. development.rbの編集
  5. pumaのインストール

 なお、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)。

data: This is the first message.

data: This is the second message, it
data: has two lines.

data: This is the third message.

 送信データは各行「(要素名): (値)」の形となり、また空行の直前までを一つの送信単位とします。この例では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の時と同様の動きをすることが確認できるかと思います。

fig02


 またChromeの開発者ツールにてNetworkタブを見てみると、「stream」のコネクションが切断されずにいるのを確認することができます。

fig03


まとめ

 ここまで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