Асинхронная обработка запросов¶
Основные методы для отправки ответа сервером:
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):
respondWebSocketText
respondWebSocketBinary
respondWebSocketPing
respondWebSocketClose
Метод 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 сообщений используйте методы:
respondSockJsText
respondSockJsClose
Обычно использование кук не подходит для 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" по умолчанию