非同期レスポンス ================ Actionからクライアントへレスポンスを返すには以下のメソッドを使用します * ``respondView``: レイアウトファイルを使用または使用せずに、Viewテンプレートファイルを送信します * ``respondInlineView``: レイアウトファイルを使用または使用せずに、インライン記述されたテンプレートを送信します * ``respondText("hello")``: レイアウトファイルを使用せずに文字列を送信します * ``respondHtml("...")``: contentTypeを"text/html"として文字列を送信します * ``respondJson(List(1, 2, 3))``: ScalaオブジェクトをJSONに変換し、contentTypeを"application/json"として送信します * ``respondJs("myFunction([1, 2, 3])")`` contentTypeを"application/javascript"として文字列を送信します * ``respondJsonP(List(1, 2, 3), "myFunction")``: 上記2つの組み合わせをJSONPとして送信します * ``respondJsonText("[1, 2, 3]")``: contentTypeを"application/javascript"として文字列として送信します * ``respondJsonPText("[1, 2, 3]", "myFunction")``: `respondJs` 、 `respondJsonText` の2つの組み合わせをJSONPとして送信します * ``respondBinary``: バイト配列を送信します * ``respondFile``: ディスクからファイルを直接送信します。 `zero-copy `_ を使用するため非常に高速です。 * ``respondEventSource("data", "event")``: チャンクレスポンスを送信します Xitrumは自動でデフォルトレスポンスを送信しません。自分で明確に上記の``respondXXX``を呼ばなければなりません。 呼ばなければ、XitrumがそのHTTP接続を保持します。あとで``respondXXX``を読んでもいいです。 接続がopen状態になっているかを確認するには``channel.isOpen``を呼びます。``addConnectionClosedListener`` でコールバックを登録することもできませす。 :: addConnectionClosedListener { // 切断されました。 // リソース開放などをする。 } 非同期なのでレスポンスはすぐに送信されません。``respondXXX`` の戻り値が `ChannelFuture `_ となります。それを使って実際にレスポンスを送信されるコールバックを登録できます。 例えばレスポンスの送信あとに切断するには: :: import io.netty.channel.{ChannelFuture, ChannelFutureListener} val future = respondText("Hello") future.addListener(new ChannelFutureListener { def operationComplete(future: ChannelFuture) { future.getChannel.close() } }) より短い例: :: respondText("Hello").addListener(ChannelFutureListener.CLOSE) WebSocket --------- :: import scala.runtime.ScalaRunTime import xitrum.annotation.WEBSOCKET import xitrum.{WebSocketAction, WebSocketBinary, WebSocketText, WebSocketPing, WebSocketPong} @WEBSOCKET("echo") class EchoWebSocketActor extends WebSocketAction { def execute() { // ここでセッションデータ、リクエストヘッダなどを抽出できますが // respondTextやrespondViewなどは使えません。 // レスポンスするには以下のようにrespondWebSocketXXXを使ってください。 log.debug("onOpen") context.become { case WebSocketText(text) => log.info("onTextMessage: " + text) respondWebSocketText(text.toUpperCase) case WebSocketBinary(bytes) => log.info("onBinaryMessage: " + ScalaRunTime.stringOf(bytes)) respondWebSocketBinary(bytes) case WebSocketPing => log.debug("onPing") case WebSocketPong => log.debug("onPong") } } override def postStop() { log.debug("onClose") super.postStop() } } リクエストが来る際に上記のアクターインスタンスが生成されます。次のときにアクターが停止されます: * コネクションが切断されるとき * WebSocketのcloseフレームが受信されるまたは送信されるとき WebSocketフレームを送信するメソッド: * ``respondWebSocketText`` * ``respondWebSocketBinary`` * ``respondWebSocketPing`` * ``respondWebSocketClose`` ``respondWebSocketPong`` はありません。Xitrumがpingフレームを受信したら自動でpongフレームを 送信するからです。 上記のWebSocketアクションへのURLを取得するには: :: // Scalateテンプレートファイルなどで val url = absWebSocketUrl[EchoWebSocketActor] SockJS ------ `SockJS `_ とはWebSocketのようなAPIを提供 するJavaScriptライブラリです。WebSocketを対応しないブラウザで使います。SockJSがブラウザがの WebSocketの機能の存在を確認し、存在しない場合、他の適切な通信プロトコルへフォルバックします。 WebSocket対応ブラウザ関係なくすべてのブラウザでWebSocket APIを使いたい場合、WebSocketを 直接使わないでSockJSを使ったほうがいいです。 :: XitrumがSockJSライブラリのファイルを含めており、テンプレートなどで以下のように書くだけでいいです: :: ... html head != jsDefaults ... SockJSは `サーバー側の特別処理 `_ が必要ですが、 Xitrumがその処理をやってくれるのです。 :: import xitrum.{Action, SockJsAction, SockJsText} import xitrum.annotation.SOCKJS @SOCKJS("echo") class EchoSockJsActor extends SockJsAction { def execute() { // ここでセッションデータ、リクエストヘッダなどを抽出できますが // respondTextやrespondViewなどは使えません。 // レスポンスするには以下のようにrespondSockJsXXXを使ってください。 log.info("onOpen") context.become { case SockJsText(text) => log.info("onMessage: " + text) respondSockJsText(text) } } override def postStop() { log.info("onClose") super.postStop() } } 新しいSockJSセッションが生成されるとき上記のアクターインスタンスが生成されます。セッションが 停止されるときにアクターが停止されます。 SockJSフレームを送信するには: * ``respondSockJsText`` * ``respondSockJsClose`` `SockJsの注意事項 `_: :: クッキーがSockJsと合わないです。認証を実装するには自分でトークンを生成しSockJsページを埋め込んで、 ブラウザ側からサーバー側へSockJs接続ができたらそのトークンを送信し認証すれば良い。クッキーが 本質的にはそのようなメカニズムで動きます。 SockJSクラスタリングを構築するには :doc:`Akkaでサーバーをクラスタリングする ` 説明をご覧ください。 Chunkレスポンス ---------------- `Chunkレスポンス `_ を送信するには: 1. ``setChunked`` を呼ぶ 2. ``respondXXX`` を呼ぶ(複数回呼んでよい) 3. 最後に ``respondLastChunk`` を呼ぶ Chunkレスポンスはいろいろな応用があります。例えばメモリがかかる大きなCSVファイルを一括で生成 できない場合、生成しながら送信して良い: :: // 「Cache-Control」ヘッダが自動で設定されます: // 「no-store, no-cache, must-revalidate, max-age=0」 // // 因みに 「Pragma: no-cache」 ヘッダはレスポンスでなくリクエストのためです: // http://palizine.plynt.com/issues/2008Jul/cache-control-attributes/ setChunked() val generator = new MyCsvGenerator generator.onFirstLine { line => val future = respondText(header, "text/csv") future.addListener(new ChannelFutureListener { def operationComplete(future: ChannelFuture) { if (future.isSuccess) generator.next() } } } generator.onNextLine { line => val future = respondText(line) future.addListener(new ChannelFutureListener { def operationComplete(future: ChannelFuture) { if (future.isSuccess) generator.next() } }) } generator.onLastLine { line => val future = respondText(line) future.addListener(new ChannelFutureListener { def operationComplete(future: ChannelFuture) { if (future.isSuccess) respondLastChunk() } }) } generator.generate() 注意: * ヘッダが最初の ``respondXXX`` で送信されます。 * 末尾ヘッダがオプションで ``respondLastChunk`` に設定できます。 * :doc:`ページとアクションキャッシュ ` はchunkレスポンスとは使えません。 Chunkレスポンスを ``ActorAction`` の組み合わせて `Facebook BigPipe `_ が実装できます。 無限iframe ~~~~~~~~~~~ Chunkレスポンスで `Comet `_ を 実装することが `可能 `_ です。 Iframeを含めるページ: :: ... ... ... 無限 ``\n") そのあと実際データを送信するには: :: if (channel.isOpen) respondText("\n") else // 切断されました。リソースなどを開放。 // ``addConnectionClosedListener``を使って良い。 Event Source ~~~~~~~~~~~~ 参考: http://dev.w3.org/html5/eventsource/ Event SourceはデータがUTF-8でchunkレスポンスの一種です。 Event Sourceをレスポンスするには ``respondEventSource`` を呼んでください(複数回可): :: respondEventSource("data1", "event1") // イベント名が「event1」となります respondEventSource("data2") // イベント名がデフォルトで「message」となります