Асинхронная обработка запросов
==============================
Основные методы для отправки ответа сервером:
* ``respondView``: при ответе использует шаблон ассоциированный с контроллером
* ``respondInlineView``: при ответе использует шаблон переданный как аргумент
* ``respondText("hello")``: ответ строкой "plain/text"
* ``respondHtml("...")``: ответ строкой "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):
* ``respondWebSocketText``
* ``respondWebSocketBinary``
* ``respondWebSocketPing``
* ``respondWebSocketClose``
Метод respondWebSocketPong не предусмотрен, потому что Xitrum автоматически отправляет "pong" сообщение в ответ на "ping".
Для получения ссылки на контроллер:
::
val url = absWebSocketUrl[EchoWebSocketActor]
SockJS
------
`SockJS `_ предоставляет JavaScript объект
эмитирующий поддержку WebSocket, для браузеров которые не поддерживают этот стандарт.
SockJS пытается использовать WebSocket если он доступен в браузере. В другом случае
будет создан эмитирующий объект.
Если вы хотите использовать WebSocket API во всех браузерах, то следует использовать
SockJS вместо WebSocket.
::
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 сообщений используйте методы:
* ``respondSockJsText``
* ``respondSockJsClose``
`Рекомендации по реализации `_:
::
Обычно использование кук не подходит для SockJS. Если вам нужна авторизация внутри сессии, то
для каждой страницы присвойте токен и используйте его в SockJS сессии, для валидации на серверной стороне.
В сущности это повторение механизма куки для SockJS.
Подробнее о настройке кластера SockJS, смотрите раздел :doc:`Кластерезация с Akka `.
Chunked ответ
-------------
Для отправки `chunked ответа `_:
1. Вызовите метод ``setChunked``
2. Отправляйте данные методами ``respondXXX``, столько раз сколько нужно
3. Последний ответ отправьте методом ``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``
* :doc:`Кэш страницы и контроллера ` не может быть использован совместно с chunked ответами.
Используя chunked ответ вместе с ``ActorAction``, легко реализовать
`Facebook BigPipe `_.
Бесконечный iframe
~~~~~~~~~~~~~~~~~~
Chunked ответ `может быть использован `_
для реализации `Comet `_.
Страница которая включает iframe:
::
...
...
...
Контроллер который последовательно отправляет ``\n")
Позднее, когда вам нужно отправить данные браузеру, просто используйте шаблон:
::
if (channel.isOpen)
respondText("\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" по умолчанию