目次
AlWorker サーバーからの情報を画面に表示する
サーバー側で発生する何らかの情報を、クライアント画面(ウェブブラウザ)に表示するためには、従来ポーリングしか方法がありませんでした。しかし、ポーリングにはいくつかの課題が残ります。
- リアルタイム性が、ポーリングサイクルに制限される。
- イベントが発生していない時もポーリングのための通信が発生するため、サーバー及びネットワークへの負荷が大きい。
- 前回のポーリングから現在までに発生した全てのイベントを、もれなくクライアントに伝えるには、相応の工夫が必要になる。
Aloneでは、これに対し2種類の方法を提供します。
- Long poll (COMET)による方法
- ServerSentEventsによる方法
Long poll (COMET)による方法
COMETとは、ポーリングベースですが、サーバー側の応答をすぐに返さず保留しておき、何らかのイベントが発生した時に応答することを繰り返すことで、擬似的にサーバープッシュを実現する技術です。
サーバーで発生したイベントを、もれなくクライアントへ伝えるために、番号付きメッセージキューを使います。
サンプル
サーバーでランダム時間で発生するイベントを、画面上にリアルタイムで表示する。
サーバー
- comet_server.rb
require "al_worker_ipc" require "al_worker_message" class CometServer < AlWorker def initialize2() @ipc = Ipc.new() @ipc.chmod = 0666 @ipc.run( self ) @msg = NumberedMessage.new() end ## # アイドルタスクで、ランダム時間でメッセージ(イベント)を発生させる # def idle_task() loop do sleep rand(10) @msg.send( {"time"=>Time.now.to_s} ) end end ## # クライアント(ブラウザ, JavaScript)からのリクエストを受け、 # メッセージ(イベント)の配列を返す。 # listen {"TID":n} # def ipc_a_listen( sock, param ) tid = param["TID"].to_i if tid > 0 # TIDが有効なら、キュー内TID以降のメッセージを返す。 # もしまだTID番が発生していなければ、ここでウェイトする。 ret = @msg.receive( tid ) else # TIDが無効なら、初期アクセスとみなして全メッセージを返す。 ret = @msg.queue.dup() end reply( sock, 200, "OK", ret ) end end server = CometServer.new( "comet_server" ) server.parse_option() server.daemon()
Alone cgiコントローラ
- main.rb
require "al_template" require "al_form" require "al_worker_ipc" class AlController include AlWorker::IpcAction def initialize() @ipc = AlWorker::Ipc.open( "/tmp/comet_server" ) end def action_index() AlTemplate.run( 'index.rhtml' ) end end
画面テンプレートとJavaScript
- index.rhtml
<%= header_section %> <script type="text/javascript" src="/js/alone.js"></script> <%= body_section %> サーバー発生イベント <div id="event_display" style="border: 1px solid black"> </div> <script type="text/javascript"> function listen( tid ) { var e = document.getElementById( "event_display" ); var ipc = new Alone.Ipc(); // コールバック // (ここで接続は一旦切れている) ipc.success = function( data, status ) { var tid = 0; // 前回のTIDから現在までに発生した全てのイベントが // 配列として data に渡されるので、繰り返して表示する。 for( var i = 0; i < data.length; i++ ) { var html = Alone.escape_html( data[i].time ) + '<br>'; e.innerHTML = e.innerHTML + html; tid = data[i].TID; } // データの最終TID+1を渡して、次回そこからのイベント送信を // 依頼することで、取りこぼしがなくなる。 listen( tid + 1 ); }; // IPC開始 ipc.call( "listen", {TID: tid} ); } // サーバへ問い合わせ開始 // 初回は無効なTIDを指定して、キューの全データを取得する。 listen( 0 ); </script> <%= footer_section %>
注意点
- イベントの間隔が長く開く場合(例えば3分間)、ブラウザやウェブサーバー、プロキシーサーバー、NATBOXなどが接続を強制切断する場合があります。
- そのため、実用的には定期的にアイドルイベントを流すなどして、自主的に接続を保つ必要があります。
プロトコル詳細
cgiリクエスト
リクエストURL例
http://*/cgi-bin/index.rb?ctrl=comet&action=ipc&ipc=listen&arg={"TID":5}
action=ipc は固定値です。ipc= は、サーバー側受付IPC名を指定します。
JavaScript中では、以下の通りライブラリを使用してIPC callします。
ipc.call( "listen", {TID: tid} );
戻り値
レスポンスは、要素2の配列をJSONエンコードしたものが返ります。
例
["200. OK", [{"time":"2013-06-04 12:13:20 +0900","TID":5},{"time":"2013-06-04 12:13:26 +0900","TID":6}]]
「JavaScriptからIPCをコールする」のプロトコル詳細も併せて参照してください。
ServerSentEventsによる方法
ブラウザがサポートしていれば、Server Sent Events ( w3.org ) を使う方法が以下の点で優れています。
- 接続が連続する。(COMETはイベントのたびに切断/接続を繰り返す)
- そのため、負荷が少なくリアルタイム性に優れている。
- 切断が切れた場合、ブラウザが自動的に再接続してくれる。
サンプル
サーバーでランダム時間で発生するイベントを、画面上にリアルタイムで表示する。
サーバー
- ssev_server.rb
require "al_worker_ipc" require "al_worker_message" class SsevServer < AlWorker def initialize2() @ipc = Ipc.new() @ipc.chmod = 0666 @ipc.run( self ) @msg = NumberedMessage.new() end ## # アイドルタスクで、ランダム時間でメッセージ(イベント)を発生させる # def idle_task() loop do sleep rand(10) @msg.send( {"time"=>Time.now.to_s} ) end end ## # クライアント(ブラウザ, JavaScript)からのリクエストを受け、 # メッセージ(イベント)をServerSentEventsプロトコルで返す。 # def ipc_a_listen_ssev( sock, param ) tid = param["LAST_EVENT_ID"] == 0 ? @msg.tid : param["LAST_EVENT_ID"] @msg.cycle( tid + 1 ) { |m| sock.puts "id: #{m[:TID].to_i}" sock.puts "data: #{m['time']}" sock.puts "" } end end server = SsevServer.new( "ssev_server" ) server.parse_option() server.daemon()
Alone cgiコントローラ
- main.rb
require "al_template" require "al_form" require "al_worker_ipc" class AlController include AlWorker::IpcAction def initialize() @ipc = AlWorker::Ipc.open( "/tmp/ssev_server" ) end def action_index() AlTemplate.run( 'index.rhtml' ) end end
画面テンプレートとJavaScript
- index.rhtml
<%= header_section %> <script type="text/javascript" src="/js/alone.js"></script> <%= body_section %> サーバー発生イベント <div id="event_display" style="border: 1px solid black"> </div> <script type="text/javascript"> function listen_ssev() { var e = document.getElementById( "event_display" ); // action:"ssev"は固定値。 // ipc:は、サーバー側受付IPC名を指定する。 var uri = Alone.make_uri({ action:"ssev", ipc:"listen_ssev" }); var evs = new EventSource( uri ); evs.onmessage = function( ev ) { var html = Alone.escape_html( ev.data ) + '<br>'; e.innerHTML = e.innerHTML + html; } } // サーバへ問い合わせ開始 listen_ssev(); </script> <%= footer_section %>
注意点
- COMET同様、他要因によって接続が切断される場合がありますが、ほとんどの場合はブラウザが自動的に再接続を行います。
- ただし、接続が切れてから実際にブラウザが再接続するまでタイムラグがある場合がありますので、定期的なアイドルイベントを併用するのは良いアイデアです。
ServerSentEvents 詳細
cgiリクエスト
リクエストURL例
GET http://*/cgi-bin/index.rb?ctrl=ssev&action=ssev&ipc=listen_ssev
action=ssev は固定値です。ipc= は、サーバー側受付IPC名を指定します。
JavaScript中では、以下の通り、ライブラリを使用してURLを生成します。
var uri = Alone.make_uri({ action:"ssev", ipc:"listen_ssev" });
ブラウザによる再接続時のリクエストには、ブラウザによって自動的に LAST_EVENT_ID パラメータがリクエストヘッダに付与されます。
戻り値
ServerSentEventsプロトコルでデータを返します。
例
id: 1 data: xxxxxx (改行) id: 2 data: yyyyyy
LAST_EVENT_IDは、Aloneフレームワークによってパラメータから取得できるように構成されますので、param["LAST_EVENT_ID"] を参照して送信済み/未送信イベントを識別します。