비동기 응답¶
Action에서 클라이언트로 응답을 반환하려면 다음과 같은 방법을 사용합니다.
respondView
: 레이아웃 파일을 사용 또는 사용하지 않고 View의 템플릿 파일을 보냅니다.respondInlineView
: 레이아웃 파일을 사용 또는 사용하지 않고 인라인 작성된 템플릿을 보냅니다.respondText("hello")
: 레이아웃 파일을 사용하지 않고 텍스트를 보냅니다.respondHtml("<html>...</html>")
: 위와 같이 contentType을 “text/html”로 보냅니다.respondJson(List(1, 2, 3))
: Scala 객체를 JSON 으로 변환하여 contentType을 “application/json”으로 보냅니다.respondJs("myFunction([1, 2, 3])")
contentType을 “application/javascript”으로 보냅니다.respondJsonP(List(1, 2, 3), "myFunction")
: 위 두가지 조합을 JSONP 으로 보냅니다.respondJsonText("[1, 2, 3]")
: contentType을 “application/javascript”으로 보냅니다.respondJsonPText("[1, 2, 3]", "myFunction")
: respondJs, respondJsonText 의 두 가지 조합을 JSONP로 보냅니다.respondBinary
: 바이트 배열로 보냅니다.respondFile
: 디스크에서 파일을 직접 보냅니다. zero-copy 를 사용하기 때문에 빠릅니다.respondEventSource("data", "event")
: 청크 응답을 보냅니다.
Xitrum은 자동으로 어떤 특정한 응답을 하지 않습니다. 스스로 응답을 respondXXX
형식으로 명시해야 합니다.
respondXXX
을 호출하지 않을 경우 Xitrum은 HTTP 연결을 유지하기 때문에 나중에 respondXXX
형식의 호출문이 필요합니다.
연결이 open 상태로 되어 있는지 확인하려면 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() {
// 여기에서 세션데이터, 요청해더 등을 추출할 수 있지만
// respondText 나 respondView를 사용하면 안됩니다.
// 응답하려면 다음과 같이 respondWebSocketXXX를 사용하세요.
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의 close 프레임이 수신되거나 전송되었을 때
WebSocket 프레임을 전송하는 경우:
respondWebSocketText
respondWebSocketBinary
respondWebSocketPing
respondWebSocketClose
respondWebSocketPong
은 없습니다. Xitrum이 ping 을 수신하게 되면 자동으로 pong 프레임을 전송하기 때문입니다.
위의 WebSocket 액션의 URL을 얻으려면:
// Scalate 템플릿 파일을 사용하기 원한다면
val url = absWebSocketUrl[EchoWebSocketActor]
SockJS¶
- SockJS 은 WebSocket을 지원하지 않는 브라우저를 위한
WebSocket 과 같은 API를 제공하는 JavaScript라이브러리 입니다. 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은 SockJS의 JavsScript 파일을 내포하고 있습니다. 뷰 템플릿에서 다음과 같이 사용하면 됩니다:
...
html
head
!= jsDefaults
...
SockJS는 server counterpart 가 필요하지 않습니다. Xitrum이 자동으로 제공합니다.
import xitrum.{Action, SockJsAction, SockJsText}
import xitrum.annotation.SOCKJS
@SOCKJS("echo")
class EchoSockJsActor extends SockJsAction {
def execute() {
// 응답을 위해 아래에 respondSockJSXXX를 사용합니다.
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 클러스터링을 구현하려면 Akka 클러스터링 을 참고하세요.
Chunk응답¶
Chunk응답 을 보내려면:
setChunked
호출respondXXX
호출(필요한 만큼)마지막으로
respondLastChunk
호출
Chunk응답은 많은 유스케이스를 가지고 있습니다. 예를 들어 메모리에 맞지 않는 매우큰 CSV파일을 생성할때 Chunk별로 생성해서 보낼 수 있습니다.
// "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
에 설정할 수 있습니다.페이지와 액션 캐쉬 는 chunk 응답으로 사용할 수 없습니다.
Chunk응답을 ActorAction
과 함께 사용하려면
Facebook BigPipe
을 통해 쉽게 구현할 수 있습니다.
무한 iframe¶
청크 응답은 Comet 을 사용할 수 있습니다
iframe을 포함한 페이지:
...
<script>
var functionForForeverIframeSnippetsToCall = function() {...}
</script>
...
<iframe width="1" height="1" src="path/to/forever/iframe"></iframe>
...
무한 <script>
생성하는 페이지:
// 준비
setChunked()
// Firefox를 동작하기 위해 "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는 특별한 경우 chunk응답을 보냄. 데이터는 UTF-8 이어야 함.
Event Source를 응답하려면 respondEventSource
호출(필요한 만큼):
respondEventSource("data1", "event1") // event1의 이벤트 이름
respondEventSource("data2") // message라는 이벤트 이름으로 기본설정됨