Xitrum学习笔记09 - 异步响应

时间:2022-12-26 21:02:35

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