Async response¶
List of normal responding methods:
respondView
: responds view template file, with or without layoutrespondInlineView
: responds embedded template (not separate template file), with or without layoutrespondText("hello")
: responds a string without layoutrespondHtml("<html>...</html>")
: same as above, with content type set to “text/html”respondJson(List(1, 2, 3))
: converts Scala object to JSON object then respondsrespondJs("myFunction([1, 2, 3])")
respondJsonP(List(1, 2, 3), "myFunction")
: combination of the above tworespondJsonText("[1, 2, 3]")
respondJsonPText("[1, 2, 3]", "myFunction")
respondBinary
: responds an array of bytesrespondFile
: sends a file directly from disk, very fast because zero-copy (aka send-file) is usedrespondEventSource("data", "event")
Xitrum does not automatically send any default response.
You must explicitly call respondXXX
methods above to send response.
If you don’t call respondXXX
, Xitrum will keep the HTTP connection for you,
and you can call respondXXX
later.
To check if the connection is still open, call channel.isOpen
.
You can also use addConnectionClosedListener
:
addConnectionClosedListener {
// The connection has been closed
// Unsubscribe from events, release resources etc.
}
Because of the async nature, the response is not sent right away.
respondXXX
returns
ChannelFuture.
You can use it to perform actions when the response has actually been sent.
For example, if you want to close the connection after the response has been sent:
import io.netty.channel.{ChannelFuture, ChannelFutureListener}
val future = respondText("Hello")
future.addListener(new ChannelFutureListener {
def operationComplete(future: ChannelFuture) {
future.getChannel.close()
}
})
Or shorter:
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()
}
}
An actor will be created when there’s request. It will be stopped when:
The connection is closed
WebSocket close frame is received or sent
Use these to send WebSocket frames:
respondWebSocketText
respondWebSocketBinary
respondWebSocketPing
respondWebSocketClose
There’s no respondWebSocketPong, because Xitrum will automatically send pong frame for you when it receives ping frame.
To get URL to the above WebSocket action:
// Probably you want to use this in Scalate view etc.
val url = absWebSocketUrl[EchoWebSocketActor]
SockJS¶
SockJS is a browser JavaScript library that provides a WebSocket-like object, for browsers that don’t support WebSocket. SockJS tries to use WebSocket first. If that fails it can use a variety of ways but still presents them through the WebSocket-like object.
If you want to work with WebSocket API on all kind of browsers, you should use SockJS and avoid using WebSocket directly.
<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 includes the JavaScript file of SockJS. In your view template, just write like this:
...
html
head
!= jsDefaults
...
SockJS does require a server counterpart. Xitrum automatically does it for you.
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()
}
}
An actor will be created when there’s a new SockJS session. It will be stopped when the SockJS session is closed.
Use these to send SockJS frames:
respondSockJsText
respondSockJsClose
See Various issues and design considerations:
Basically cookies are not suited for SockJS model. If you want to authorize a
session, provide a unique token on a page, send it as a first thing over SockJS
connection and validate it on the server side. In essence, this is how cookies
work.
To config SockJS clustering, see Clustering with Akka.
Chunked response¶
To send chunked response:
Call
setChunked
Call
respondXXX
as many times as you wantLastly, call
respondLastChunk
Chunked response has many use cases. For example, when you need to generate a very large CSV file that does may not fit memory, you can generate chunk by chunk and send them while you generate:
// "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()
Notes:
Headers are sent on the first
respondXXX
call.You can send optional trailing headers at
respondLastChunk
Page and action cache cannot be used with chunked response.
Using chunked response together with ActorAction
, you can easily implement
Facebook BigPipe.
Forever iframe¶
Chunked response can be used for Comet.
The page that embeds the iframe:
...
<script>
var functionForForeverIframeSnippetsToCall = function() {...}
</script>
...
<iframe width="1" height="1" src="path/to/forever/iframe"></iframe>
...
The action that responds <script>
snippets forever:
// 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")
Later, whenever you want to pass data to the browser, just send a snippet:
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¶
See http://dev.w3.org/html5/eventsource/
Event Source response is a special kind of chunked response. Data must be UTF-8.
To respond event source, call respondEventSource
as many time as you want.
respondEventSource("data1", "event1") // Event name is "event1"
respondEventSource("data2") // Event name is set to "message" by default