====== AlWorker サーバーからの情報を画面に表示する ======
サーバー側で発生する何らかの情報を、クライアント画面(ウェブブラウザ)に表示するためには、従来ポーリングしか方法がありませんでした。しかし、ポーリングにはいくつかの課題が残ります。
* リアルタイム性が、ポーリングサイクルに制限される。
* イベントが発生していない時もポーリングのための通信が発生するため、サーバー及びネットワークへの負荷が大きい。
* 前回のポーリングから現在までに発生した全てのイベントを、もれなくクライアントに伝えるには、相応の工夫が必要になる。
Aloneでは、これに対し2種類の方法を提供します。
* Long poll (COMET)による方法
* ServerSentEventsによる方法
====== Long poll (COMET)による方法 ======
COMETとは、ポーリングベースですが、サーバー側の応答をすぐに返さず保留しておき、何らかのイベントが発生した時に応答することを繰り返すことで、擬似的にサーバープッシュを実現する技術です。\\
サーバーで発生したイベントを、もれなくクライアントへ伝えるために、番号付きメッセージキューを使います。\\
===== サンプル =====
サーバーでランダム時間で発生するイベントを、画面上にリアルタイムで表示する。
==== サーバー ====
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コントローラ ====
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 ====
<%= header_section %>
<%= body_section %>
サーバー発生イベント
<%= 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}]]
[[alworker:javascriptからIPCをコールする#プロトコル詳細|「JavaScriptからIPCをコールする」のプロトコル詳細]]も併せて参照してください。
====== ServerSentEventsによる方法 ======
ブラウザがサポートしていれば、Server Sent Events ( [[http://dev.w3.org/html5/eventsource/|w3.org]] ) を使う方法が以下の点で優れています。
* 接続が連続する。(COMETはイベントのたびに切断/接続を繰り返す)
* そのため、負荷が少なくリアルタイム性に優れている。
* 切断が切れた場合、ブラウザが自動的に再接続してくれる。
===== サンプル =====
サーバーでランダム時間で発生するイベントを、画面上にリアルタイムで表示する。
==== サーバー ====
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コントローラ ====
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 ====
<%= header_section %>
<%= body_section %>
サーバー発生イベント
<%= 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"] を参照して送信済み/未送信イベントを識別します。