Асинхронная обработка запросов¶
Основные методы для отправки ответа сервером:
respondView: при ответе использует шаблон ассоциированный с контроллеромrespondInlineView: при ответе использует шаблон переданный как аргументrespondText("hello"): ответ строкой «plain/text»respondHtml("<html>...</html>"): ответ строкой «text/html»respondJson(List(1, 2, 3)): преобразовать Scala объект в JSON и ответитьrespondJs("myFunction([1, 2, 3])")respondJsonP(List(1, 2, 3), "myFunction"): совмещение предыдущих двухrespondJsonText("[1, 2, 3]")respondJsonPText("[1, 2, 3]", "myFunction")respondBinary: ответ массивом байтrespondFile: переслать файл с использованием техники zero-copy (aka send-file)respondEventSource("data", "event")
Xitrum автоматически не осуществляет отправку ответа клиенту. Вы должны явно вызвать один из методов respondXXX
что бы отправить ответ клиенту. Если вы не вызовете метод``respondXXX``, Xitrum будет поддерживать HTTP соединение,
до тех пор пока не будет вызван метод respondXXX.
Что бы убедиться что соединение открыто используйте метод 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() {
// 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()
}
}
Актор будет создан при открытии подключения. И остановлен когда:
Соединение будет разорвано
WebSocket закроет подключение
Используйте следующие методы для отправки WebSocket сообщений (frame):
respondWebSocketTextrespondWebSocketBinaryrespondWebSocketPingrespondWebSocketClose
Метод respondWebSocketPong не предусмотрен, потому что Xitrum автоматически отправляет «pong» сообщение в ответ на «ping».
Для получения ссылки на контроллер:
val url = absWebSocketUrl[EchoWebSocketActor]
SockJS¶
SockJS предоставляет JavaScript объект эмитирующий поддержку WebSocket, для браузеров которые не поддерживают этот стандарт. SockJS пытается использовать 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 включает файл SockJS по умолчанию. В шаблоне следует написать:
...
html
head
!= jsDefaults
...
SockJS подразумевает наличие части реализации на сервере. Xitrum автоматически ее реализует:
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()
}
}
Актор будет создан при открытии новой SockJS сессии. И остановлен когда сессия будет закрыта.
Для отправки SockJS сообщений используйте методы:
respondSockJsTextrespondSockJsClose
Обычно использование кук не подходит для SockJS. Если вам нужна авторизация внутри сессии, то
для каждой страницы присвойте токен и используйте его в SockJS сессии, для валидации на серверной стороне.
В сущности это повторение механизма куки для SockJS.
Подробнее о настройке кластера SockJS, смотрите раздел Кластерезация с Akka.
Chunked ответ¶
Для отправки chunked ответа:
Вызовите метод
setChunkedОтправляйте данные методами
respondXXX, столько раз сколько нужноПоследний ответ отправьте методом
respondLastChunk
Chunked ответы имеют множество применений. Например, когда нужно генерировать большой документ который не помещается в памяти, вы можете генерировать этот документ частями и отправлять их последовательно:
// "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Кэш страницы и контроллера не может быть использован совместно с chunked ответами.
Используя chunked ответ вместе с ActorAction, легко реализовать
Facebook BigPipe.
Бесконечный iframe¶
Chunked ответ может быть использован для реализации Comet.
Страница которая включает iframe:
...
<script>
var functionForForeverIframeSnippetsToCall = function() {...}
</script>
...
<iframe width="1" height="1" src="path/to/forever/iframe"></iframe>
...
Контроллер который последовательно отправляет <script>:
// Подготовка к вечному iframe
setChunked()
// Необходимо отправить например "123" для некоторых браузеров
respondText("<html><body>123", "text/html")
// Большинство клиентов (даже curl!) не выполняют тело <script> немедленно,
// необходимо отправить около 2KB данных что бы обойти эту проблему
for (i <- 1 to 100) respondText("<script></script>\n")
Позднее, когда вам нужно отправить данные браузеру, просто используйте шаблон:
if (channel.isOpen)
respondText("<script>parent.functionForForeverIframeSnippetsToCall()</script>\n")
else
// Соединение было закрыто, необходимо освободить ресурсы
// Вы можете использовать так же ``addConnectionClosedListener``.
Event Source¶
Смотри http://dev.w3.org/html5/eventsource/
Event Source ответ, это специальный тип chunked ответа. Данные должны быть в кодировке UTF-8.
Для ответа в формате event source, используйте метод respondEventSource столько раз сколько нужно.
respondEventSource("data1", "event1") // Имя события "event1"
respondEventSource("data2") // Имя события устанавливается в "message" по умолчанию