Xitrum学习笔记09 - 异步响应
Xitrum不会自动发送默认响应,必须调用respondXXX方法发送响应。如果没有调用respondXXX,Xitrum会保持HTTP连接,可以过后调用respondXXX。
调用 channel.isOpen 来检查 HTTP连接 是否还处于打开状态。还可以使用addConnectionClosedListener方法,定义连接关闭之后的动作。
addConnectionClosedListener { // The connection has been closed // Unsubscribe from events, release resources etc. }
因为异步性质,响应不会立刻被发送。respondXXX返回io.netty.channel.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() { // Here you can extract session data, request headers etc. // but do not use respondText, respondView etc. // To respond, use respondWebSocketXXX like below. 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() } }
一个Actor在由请求的时候会被创建,它在以下情况下回被停止:连接关闭;WebSocket close frame被接收或发送。
用来发送WebSocket frames的方法:
• respondWebSocketText
• respondWebSocketBinary
• respondWebSocketPing
• respondWebSocketClose
当Xitrum接收ping frame时,会自动发送pong frame,所以没有respondWebSocketPong的方法
获取WebSocket action的URL:
// Probably you want to use this in Scalate view etc. val url = absWebSocketUrl[EchoWebSocketActor]
SockJS
SockJS是一个浏览器端的JavaScript库,为不支持WebSocket的浏览器提供类似于WebSocket的对象。
SockJS会先尝试使用WebSocket,如果失败则会使用类似于WebSocket的对象。
如果要在所有的浏览器都是用WebSocket API,则应该使用SockJS而避免直接使用WebSocket
<script> var sock = new SockJS('http://mydomain.com/path_prefix'); sock.onopen = function() { console.log('open'); }; sock.onmessage = function(e) { console.log('message', e.data); }; sock.onclose = function() { console.log('close'); }; </script>
Xitrum通过在View模板中调用 jsDefaults 包含了 SockJS的JavaScript
一个Actor在出现新的SockJS session时被创建,在SockJS session关闭时被停止。
使用 respondSockJsText和respondSockJsClose发送 SockJS frame。
代码示例:
import xitrum.{Action, SockJsAction, SockJsText} import xitrum.annotation.SOCKJS @SOCKJS("echo") class EchoSockJsActor extends SockJsAction { def execute() { // To respond, use respondSockJsXXX like below log.info("onOpen") context.become { case SockJsText(text) => log.info("onMessage: " + text) respondSockJsText(text) } } override def postStop() { log.info("onClose") super.postStop() } }
分块响应
要发送分块响应
1. 调用 setChunked
2. 多次调用 respondXXX
3. 最后,调用 respondLastChunk
分块响应示例,创建一个很大的CSV文件
// "Cache-Control" header will be automatically set to: // "no-store, no-cache, must-revalidate, max-age=0" // // Note that "Pragma: no-cache" is linked to requests, not responses: // 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中发送
• 页面和action缓存不能被以分块响应的形式使用
永存的IFrame
Forever Iframe(永存的Iframe)技术涉及了一个置于页面中的隐藏Iframe标签,该标签的src属性指向返回服务器端事件的servlet路径。
每次在事件到达时,servlet写入并刷新一个新的script标签,该标签内部带有JavaScript代码,iframe的内容被附加上这一script标签,标签中的内容就会得到执行。
在页面中嵌入iframe:
... <script> var functionForForeverIframeSnippetsToCall = function() {...} </script> ... <iframe width="1" height="1" src="path/to/forever/iframe"></iframe> ...
script标签的代码片段
// Prepare forever iframe setChunked() // Need something like "123" for Firefox to work respondText("<html><body>123", "text/html") // Most clients (even curl!) do not execute <script> snippets right away, // we need to send about 2KB dummy data to bypass this problem for (i <- 1 to 100) respondText("<script></script>\n")
过后,要传递数据到浏览器时,发送这个代码片段
if (channel.isOpen) respondText("<script>parent.functionForForeverIframeSnippetsToCall()</script>\n") else // The connection has been closed, unsubscribe from events etc. // You can also use ``addConnectionClosedListener``.
使用Event Source实现页面消息推送
Server-sent events(SSE)是一种能让浏览器通过HTTP连接自动收到服务器端更新的技术,SSE EventSource 接口被W3C制定为HTML5的一部分。
Event Source响应是一种特殊的分块响应,数据必须是UTF-8的
可以多次调用respondEventSource以响应event source
respondEventSource("data1", "event1") // Event name is "event1" respondEventSource("data2") // Event name is set to "message" by default