비동기 응답 ================ Action에서 클라이언트로 응답을 반환하려면 다음과 같은 방법을 사용합니다. * ``respondView``: 레이아웃 파일을 사용 또는 사용하지 않고 View의 템플릿 파일을 보냅니다. * ``respondInlineView``: 레이아웃 파일을 사용 또는 사용하지 않고 인라인 작성된 템플릿을 보냅니다. * ``respondText("hello")``: 레이아웃 파일을 사용하지 않고 텍스트를 보냅니다. * ``respondHtml("...")``: 위와 같이 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을 직접 사용하지 마세요. :: 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를 통해 서버 측에서 검증을 해야 합니다. 이것이 본질적으로 쿠키의 작동원리입니다 SockJS 클러스터링을 구현하려면 :doc:`Akka 클러스터링 ` 을 참고하세요. Chunk응답 -------- `Chunk응답 `_ 을 보내려면: 1. ``setChunked`` 호출 2. ``respondXXX`` 호출(필요한 만큼) 3. 마지막으로 ``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`` 에 설정할 수 있습니다. * :doc:`페이지와 액션 캐쉬 ` 는 chunk 응답으로 사용할 수 없습니다. Chunk응답을 ``ActorAction`` 과 함께 사용하려면 `Facebook BigPipe `_ 을 통해 쉽게 구현할 수 있습니다. 무한 iframe ~~~~~~~~~~~ 청크 응답은 `Comet `_ 을 `사용할 수 있습니다 `_ iframe을 포함한 페이지: :: ... ... ... 무한 ``\n") 나중에 실제 데이터를 브라우저에 보내려면, 미리보기를 보내면 된다: :: if (channel.isOpen) respondText("\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라는 이벤트 이름으로 기본설정됨