Play 2.0 用戶指南 - 異步HTTP編程 --針對Scala開發者

處理異步結果


    爲何須要異步結果?

   
    目前爲止,咱們可以直接向客戶端發送響應。

    然而狀況不老是這樣:結果可能依賴於一個繁重的計算和一個長時間的web service調用。

    緣於 Play 2.0 的工做方式,action代碼必須儘量的快(如,非阻塞)。那,未能生成最終結果前,應該返回什麼呢?答案是返回一個 promise(承諾?) of response!
    A Promise [Result] 最終會贖回一個Result類型的值。使用 Promise[Result] 替換正常的Result,咱們能夠無阻塞的快速生成結果。該響應是一個返回Result的承諾(Promise)。

    等待響應的時候,web客戶端將會被阻塞,但服務器不會被阻塞,空閒資源能夠移作它用。
   

    怎樣建立Promise[Result]

   

    爲了建立Promise[Result],咱們首先須要另外一個promise:該promise將爲咱們計算實際的結果值。javascript

val promiseOfPIValue: Promise[Double] = computePIAsynchronously()
val promiseOfResult: Promise[Result] = promiseOfPIValue.map { pi =>
  Ok("PI value computed: " + pi)    
}

    全部的 Play 2.0 的異步調用API會返回 Promise。無論你是使用 play.api.libs.WS API調用外部web服務,仍是藉助Akka分配異步任務,亦或使用 play.api.libs.Akka 在actors間通訊。

    一個簡單的異步執行代碼塊並獲取一個 Promise 對象的方法是使用 play.api.libs.concurrent.Akka助手:
val promiseOfInt: Promise[Int] = Akka.future {
  intensiveComputation()
}


    注意:該intensiveComputation計算單元將運行在另外一個線程中,或者運行位於Akka集羣的遠程服務器中。
   

    異步結果


    迄今爲止,咱們都使用 SimpleResult 來發送一個異步響應,咱們須要一個 AsyncResult 類來封裝實際的 SimpleReslut:
def index = Action {
  val promiseOfInt = Akka.future { intensiveComputation() }
  Async {
    promiseOfInt.map(i => Ok("Got result: " + i))
  }
}

    注意:Async { ... }是一個助手方法,用於從Promise[Result]中構建AsyncResult。
   

    處理超時


    超時處理,經常用於避免瀏覽器因某些錯誤而遭長時間阻塞。這種情形很容易處理:
def index = Action {
  val promiseOfInt = Akka.future { intensiveComputation() }
  Async {
    promiseOfInt.orTimeout("Oops", 1000).map { eitherIntorTimeout =>
      eitherIorTimeout.fold(
        timeout => InternalServerError(timeout),
        i => Ok("Got result: " + i)    
      )    
    }  
  }
}


    HTTP流響應


    標準響應和Content-Length

    從HTTP 1.1開始,爲保證單個打開的鏈接能服務於多個HTTP請求和響應,服務器必須針對響應發送適當的Content-Length請求頭。
    默認狀況,當發回一個響應結果時,你並無指定Content-Length頭信息,例如:
def index = Action {
  Ok("Hello World")
}

    然而,由於該內容是已知的,Play可以自行計算該長度併產生適當的響應頭信息。
    注意:基於文本的內容不像表面看上去哪麼簡單, Content-Length 頭須要根據字符編碼計算,並把字符轉換成字節。

    實際上,咱們前面已經看到,response body 被指定使用一個play.api.libs.iteratee.Enumerator:
def index = Action {
  SimpleResult(
    header = ResponseHeader(200),
    body = Enumerator("Hello World")
  )
}


    意味着,爲了正確計算Content-Length,Play必須消耗整個enumerator,並把內容所有加到內存中。

    大數據發送


    若是對於簡單的Enumerators,把內容所有加載到內存不是問題,那麼大量數據怎麼辦呢?比方說咱們要給客戶端返回一個大的文件。
    咱們首先看看如何建立Enumerator [Array[Byte]]列舉該內容:
val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)

    它看起來正確嗎?咱們僅使用enumerator指定 response body:
def index = Action {

  val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    
    
  SimpleResult(
    header = ResponseHeader(200),
    body = fileContent
  )
}


    實際上這是有問題的。咱們沒有指定Content-Length長度,Play必須自行計算。惟一的方法是消耗整個enumerator,並將內容所有加載到內存中,最後方能計算響應的長度。
    對於大文件,這是有問題的。咱們不但願內容都加載到內存中。爲了不這種狀況,咱們須要手動指定Content-Length的長度。
def index = Action {

  val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    
    
  SimpleResult(
    header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)),
    body = fileContent
  )
}


    經過這種方式,Play將以懶加載的方式使用enumerator,一塊一塊的將可用數據拷貝到HTTP響應中。

    處理文件


    Play爲處理本地文件提供了一個便利方法:
def index = Action {
  Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}


    該方法也會根據文件名肯定Content-Type內容,並添加Content-Disposition元素來指定瀏覽器的處理方式。默認是經過添加Content-Disposition : attachment ; filename =fileToServe.pdf 響應頭信息,指定瀏覽器下載該文件。
    你也能夠自定義文件名:
def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    fileName = _ => "termsOfService.pdf"
  )
}


    你也能經過不指定文件名,避免瀏覽器下載該文件,而僅讓瀏覽器顯示該文件內容,如text,HTML或圖片等這些瀏覽器原生支持的文件類型。

    分塊響應


    目前爲止,發送響應前,咱們就計算好響應體長度,一切都工做得很好。可是,須要動態計算的長度怎麼辦?長度沒法獲取的狀況下怎麼辦?
    這種類型的響應,咱們必須使用Chunked transfer encoding。

    Chunked transfer encoding是HTTP 1.1提供的一種數據傳輸機制,服務器將內容分紅多塊傳送。它使用Transfer-Encoding HTTP響應頭替代Content-Length, 以跳過長度限制。因爲Content-Length再也不使用,數據在發送給客戶端(一般是web瀏覽器)前,不須要提早計算長度了。在得知內容的總長度前,服務器能夠動態的生成並傳輸內容。

    每一個塊大小在發送前被正確的指定,以便瀏覽器通知什麼時候該塊數據接收完成。數據傳輸將在最後一個長度爲零的塊處中斷。

    這種機制的優勢是咱們能夠實時的傳送數據。只要塊數據可用,咱們就發送它。缺陷是,既然內容長度沒法獲知,瀏覽器沒法顯示正確的下載進度。
    比方咱們有某個服務,利用InputStream流動態的操縱數據。首先咱們爲該流建立一個Enumerator:
val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)

    咱們如今能夠經過ChunkedResult處理這些數據:
def index = Action {

  val data = getDataStream
  val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
  
  ChunkedResult(
    header = ResponseHeader(200),
    chunks = dataContent
  )
}

    一如既往,咱們提供了便利方法完成一樣工做:
def index = Action {

  val data = getDataStream
  val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
  
  Ok.stream(dataContent)
}

    固然咱們也能夠用任何的Enumerator指定塊數據:
def index = Action {
  Ok.stream(
    Enumerator("kiki", "foo", "bar").andThen(Enumerator.eof)
  )
}

    Tip:Enumerator.callbackEnumerator and Enumerator.pushEnumerator convenient ways to create reactive non-blocking enumerators in an imperative style.

    咱們能夠查看服務器發回的響應:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

4
kiki
3
foo
3
bar
0

    咱們接收到了三塊數據,最後在接到到空塊後關閉該響應。

    Comet sockets

   
    使用 分塊 response 建立Comet Socket

    Chunked responses的其中一個用處是建立Comet sockets。一個Comet響應不過是一個包含<script>元素的 text/html 響應。每一個響應塊寫入的<script>標籤都被瀏覽器 當即執行。經過這種方式,咱們能夠實時的爲瀏覽器發送事件:每一段消息,咱們均可以包裝入<script>標籤中,聲明一個JavaScript回調函數,並寫入響應塊中。

    讓咱們開始寫第一個簡陋的協議:一個enumerator生成幾個<script>標籤,調用瀏覽器的 console.log 方法:
def comet = Action {
  val events = Enumerator(
     """<script>console.log('kiki')</script>""",
     """<script>console.log('foo')</script>""",
     """<script>console.log('bar')</script>"""
  )
  Ok.stream(events >>> Enumerator.eof).as(HTML)
}

    若是你從瀏覽器中訪問該action,你將會在瀏覽器控制檯中看到三個日誌記錄。
    提示:編寫 events >>> Enumerator.eof 只是 events.andThen(Enumerator.eof)的另外一種方式。

    咱們可使用play.api.libs.iteratee.Enumeratee更好的改寫該例子。that is just an adapter to transform an Enumerator [A] into another Enumerator
    [B] .讓咱們使用它來封裝一個標準的消息響應:
import play.api.templates.Html

// Transform a String message into an Html script tag
val toCometMessage = Enumeratee.map[String] { data => 
    Html("""<script>console.log('""" + data + """')</script>""")
}

def comet = Action {
  val events = Enumerator("kiki", "foo", "bar")
  Ok.stream(events >>> Enumerator.eof &> toCometMessage)
}

提示:編寫 events >>> Enumerator.eof &> toCometMessage 只是  events.andThen(Enumerator.eof).through(toCometMessage) 的另外一種形式。

    使用 play.api.libs.Comet 助手


    咱們提供了一個Comet助手類來處理這類Comet chunked streams,實現幾乎和咱們這裏編寫的同樣。
    注意:實際上他爲你作了更多事,考慮到瀏覽器的兼容性,他會先推送一些空數據來判斷瀏覽器的兼容性,他支持String和JSON兩種類型。它也能夠被擴展以支持更多消息類型。

    咱們使用它來重寫前面的例子:
def comet = Action {
  val events = Enumerator("kiki", "foo", "bar")
  Ok.stream(events &> Comet(callback = "console.log"))
}


    Tip:Enumerator.callbackEnumerator and Enumerator.pushEnumerator convenient ways to create reactive non-blocking enumerators in an imperative style.

    無限使用iframe技術(???)

    標準的Comet Socket技術是給一個iframe無限的推送並指定調用父窗口的回調函數數據。
def comet = Action {
  val events = Enumerator("kiki", "foo", "bar")
  Ok.stream(events &> Comet(callback = "parent.cometMessage"))
}


    HTML看起來會像這樣:
<script type="text/javascript">
  var cometMessage = function(event) {
    console.log('Received event: ' + event)
  }
</script>

<iframe src="/comet"></iframe>

   

    WebSockets


    使用WebSockets取代Comet sockets

    Comet sockets是一種向瀏覽器發送實時數據的hack技術。他僅僅提供了服務器和客戶端交流的一種形式。給服務器端發送數據,客戶端只能發送Ajax請求。

    Note: It is also possible to achieve the same kind of live communication the other way around by
    using an infinite HTTP request handled by a custom BodyParser that receives chunks of input
    data, but that is far more complicated.

    現代的瀏覽器藉助WebSockets原生支持雙向實時通訊。
   
    WebSockets是一種經過單一的TCP socket鏈接提供雙向的,全雙工鏈接通道的web技術。WebSockets API被W3C標準化,WebSockets協議也被IEF以RFC 6455形式標準化。
   
    WebSockets被設計於在瀏覽器和服務器中實現,但它其實可用於任何的服務器端或客戶端應用。因爲管理員經常拒絕本地環境之外非80端口的普通TCP鏈接,WebSockets必須找到一種方式規避該限制,還需提供一些相似的功能讓單一的TCP鏈接服務於多個WebSockets請求。

    WebSockets也爲那些須要實時,雙向通訊的應用提供幫助。WebSockets實現之前,這種雙向通訊只能使用Comet通道實現。然而Comet實現的設施並不可靠,緣於TCP的握手機制和HTTP協議開銷,對於短消息傳輸是很是低效的。
    WebSocket協議旨在解決這些問題,而不影響現有的web安全的假設妥協。
    http://en.wikipedia.org/wiki/WebSocket

    處理WebSockets

    如今爲止,咱們使用action實例處理標準HTTP請求,發送標準HTTP響應。WebSockets是如些的獨特而不能使用常規的Action處理。
    爲了處理WebSockets請求,需使用WebSocket替代Action:
def index = WebSocket.using[String] { request => 
  
  // Log events to the console
  val in = Iteratee.foreach[String](println).mapDone { _ =>
    println("Disconnected")
  }
  
  // Send a single 'Hello!' message
  val out = Enumerator("Hello!")
  
  (in, out)
}

    WebSocket具有檢索請求頭(從HTTP請求頭啓動一個WebSocket鏈接)能力,容許你取回標準頭消息和session數據。然而它不具有訪問 request body 和HTTP response body 的能力。

    當經過這種方式創建WebSocket的時候,咱們必須返回一對in和out頻道。
        in頻道是一個Iteratee[A,Unit](A是消息類型,咱們這裏使用String),它會被每個消息通知,當客戶端關閉socket的時,會收到EOF。
        out頻道是一個Enumerator[A],它將產生待發送給客戶端的消息。能夠經過服務器端發送一個EOF來關閉它。

    在該例子中,咱們簡單的將消息疊代發送給控制檯。爲了發送消息,咱們建立了個虛擬的簡單enumerator來發送 Hello! 消息。
    提示:你能夠訪問http://websocket.org/echo.html測試WebSockets。只需把地址設爲ws://localhost :9000 .

    讓咱們編寫另一個例子,丟棄輸入值,發送Hello!後關閉該socket。
def index = WebSocket.using[String] { request => 
  
  // Just consume and ignore the input
  val in = Iteratee.consume[String]()
  
  // Send a single 'Hello!' message and close
  val out = Enumerator("Hello!") >>> Enumerator.eof
  
  (in, out)
}
相關文章
相關標籤/搜索