play2.x 官網筆記擷取+翻譯 第二章(完)

fairjm發表於2015-05-20

fairjm @ ituring


第二章主要是講非同步HTTP程式設計
和第一章一樣 主要擷取內容作筆記+翻譯為主
有任何問題歡迎評論
轉截註明來自圖靈社群 http://www.ituring.com.cn/


1、Handling asynchronous results

來自 https://www.playframework.com/documentation/2.3.x/ScalaAsync

Play自下而上都是非同步的。Play以非同步非阻塞的方式處理每個請求。

在controllers理應該避免阻塞。常見的阻塞操作包括JDBC呼叫 流API HTTP請求和長時間的計算

當然也可以通過增加預設的執行緒數來允許controllers裡的阻塞程式碼承受更多的併發訪問 遵循以下的推薦 可以保持controllers的非同步以此來保持更高的擴充套件性和高負載下的可用性

建立非阻塞的actions
Play的工作方式需要action越快越好(無阻塞)。具體的做法是通過返回一個future的結果(Future[Result])作為回應。
Play將會在承諾被取回(一個Future對應一個Promise 承諾取回也就是Future得到結果,Promise和Future不是什麼新鮮的東西,但是感覺這一對還挺有詩意的..)時服務結果。

如何建立Future[Result]
所有的Play非同步API都會給你Future 例如:
play.api.libs.WS
play.api.libs.Akka

例子:

import play.api.libs.concurrent.Execution.Implicits.defaultContext
val futureInt: Future[Int] = scala.concurrent.Future {
  intensiveComputation()
}

注意上面用的執行緒池是預設的執行緒池 也就是如果要處理很耗時的任務會阻塞執行緒 那最好用其他的執行緒池 而不是play的預設執行緒池
也可以使用Actor

返回Futures

import play.api.libs.concurrent.Execution.Implicits.defaultContext
def index = Action.async {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  futureInt.map(i => Ok("Got result: " + i))
}

Action預設就是非同步的
Action.applyAction.async的處理機制是一樣的 只有一種Action型別(非同步),兩者的差別只是返回值不同。

超時機制
超時機制的實現是相當簡單的:

import play.api.libs.concurrent.Execution.Implicits.defaultContext
import scala.concurrent.duration._
def index = Action.async {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  val timeoutFuture = play.api.libs.concurrent.Promise.timeout("Oops", 1.second)
  Future.firstCompletedOf(Seq(futureInt, timeoutFuture)).map {
    case i: Int => Ok("Got result: " + i)
    case t: String => InternalServerError(t)
  }
}

可見scala本身的Future提供的功能還是很完善的 基礎設施完善 所需的額外程式碼就少了


2 Streaming HTTP responses

自HTTP 1.1以來,保持單個連線開放來處理多個HTTP請求和回應的話,伺服器必須在回應中包含一個合適的Content-Length HTTP頭

def index = Action {
  Ok("Hello World")
}

預設下,返回一個簡單的結果是不會指定Content-Length的,當然簡單的回應的傳送資訊是已知的,play也會計算這個長度並放在頭中。(要注意基於文字的內容計算長度可不是看上去那麼簡單,因為他要根據特定的字符集來計算)

上面的程式碼是下面的簡化寫法,實際用到了play.api.libs.iteratee.Enumerator:

def index = Action {
  Result(
    header = ResponseHeader(200),
    body = Enumerator("Hello World")
  )
}

這意味著要正確地計算Content-Length Play必須消費整個enumerator並且把內容載入記憶體中計算

傳送大資料量的內容
將小的資料量的內容全部讀入當然沒問題 那大資料量的怎麼辦呢?

一個錯誤的例子:

def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    

  Result(
    header = ResponseHeader(200),
    body = fileContent
  )
}

為什麼錯? 因為沒設定Content-Length play依舊要把整個吃進去計算 實在是浪費資源…

正確的姿勢:

def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    

  Result(
    header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)),
    body = fileContent
  )
}

接著Play就會用惰載入的方式消費內容了 (資料一旦可用就傳送 而不是全部內容讀入記憶體後再傳送)

提供檔案(Serving files)
play提供了一些簡便的方式來實現上面的需求:

def index = Action {
  Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}

回應中增加的內容:

Content-Disposition: attachment;filename=fileToServe.pdf

(關於Content-Disposition:http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html)

提供自定義的檔名:

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    fileName = _ => "termsOfService.pdf"
  )
}  

瀏覽器直接顯示(inline) 而不是下載:

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    inline = true
  )
}

分塊響應(Chunked responses)
上面的例子是對於可以在傳輸檔案之前得到檔案長度的
那麼如果對於動態計算的內容 事先不能知道內容長度怎麼處理呢?

要使用 Chunked transfer encoding
Chunked transfer encoding是HTTP 1.1的一種資料傳輸機制
web伺服器提供以一系列的塊的方式來提供檔案
他使用 Transfer-Encoding回應頭而不是Content-Length
因為沒有使用Content-Length 所以伺服器不需要在傳輸前知道內容的長度 可以用於傳輸動態生成的檔案

資料的結束是通過最後傳一個長度為0的chunk結束的

好處: 可以處理活資料(資料可用就立馬傳輸)
壞處: 瀏覽器就顯示不了下載進度條了

play中要實現:

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.chunked(dataContent)
}

當然裡面的Enumerator是可以自己定義的:

def index = Action {
  Ok.chunked(
    Enumerator("kiki", "foo", "bar").andThen(Enumerator.eof)
  )
}

HTTP的回應:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
4
kiki
3
foo
3
bar
0  

3 Comet sockets

Comet socket僅僅是內容為text/html只包含<script>元素的chunked回應
我們可以向瀏覽器傳送事件

def comet = Action {
  val events = Enumerator(
    """<script>console.log('kiki')</script>""",
    """<script>console.log('foo')</script>""",
    """<script>console.log('bar')</script>"""
  )
  Ok.chunked(events).as(HTML)
}

也可以用 play.api.libs.iteratee.Enumeratee
這個是一個介面卡 將Enumerator[A]轉換到Enumerator[B]

import play.twirl.api.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.chunked(events &> toCometMessage)
}

使用play.api.libs.Comet
上面的程式碼可以簡單地寫成:

def comet = Action {
  val events = Enumerator("kiki", "foo", "bar")
  Ok.chunked(events &> Comet(callback = "console.log"))
}

實際上這個幫助類幹了更多的事 比如為了瀏覽器的相容性傳送一個初始化的空白緩衝資料
支援String和JSON 可以擴充套件型別類來支援更多的訊息型別

永遠的iframe技術(The forever iframe technique)
標準的輸出Comet socket的技術是載入一個無限的chunked comet回應在iframe裡 並且呼叫父框架上的回撥函式

def comet = Action {
  val events = Enumerator("kiki", "foo", "bar")
  Ok.chunked(events &> Comet(callback = "parent.cometMessage"))
}

html頁:

<script type="text/javascript">
  var cometMessage = function(event) {
    console.log('Received event: ' + event)
  }
</script>
<iframe src="/comet"></iframe>

4 websocket

允許瀏覽器進行全雙工通訊
使用webSocket可以複用存在的play使用的TCP埠

處理 WebSocket
webSocket是個完全不同的野獸 不能用標準的Action來駕馭
play提供了兩種方式來駕馭WebSocket
第一種是使用actors
第二種是使用iteratees
兩種方式都可以通過WebSocket這個構造者獲得

通過Actor處理WebSocket
機制比較簡單 Play會給用來傳送資料的akka.actor.ActorRef 我們使用這個來建立一個akka.actor.Props

import play.api.mvc._
import play.api.Play.current
def socket = WebSocket.acceptWithActor[String, String] { request => out =>
  MyWebSocketActor.props(out)
}  

actor:

import akka.actor._
object MyWebSocketActor {
  def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
class MyWebSocketActor(out: ActorRef) extends Actor {
  def receive = {
    case msg: String =>
      out ! ("I received your message: " + msg)
  }
}  

所有從客戶端接收的資料都會傳送給上面這個actor
任何傳送給out那個ActorRef的資料都會傳送給客戶端

WebSocket關閉時 Play會自動停止那個actor
想主動關閉一個Websocket連線時也只需要關閉那個actor就行了
一顆毒藥丸:

import akka.actor.PoisonPill
self ! PoisonPill

(注:這個是觸發 ActorKilledException 預設的策略是Stop來實現的)

拒絕操作也比較簡單 改變就是方法從acceptWithActor變為了tryAcceptWithActor

import scala.concurrent.Future
import play.api.mvc._
import play.api.Play.current
def socket = WebSocket.tryAcceptWithActor[String, String] { request =>
  Future.successful(request.session.get("user") match {
    case None => Left(Forbidden)
    case Some(_) => Right(MyWebSocketActor.props)
  })
}

上面的程式碼 如果在session中沒有user這個屬性 那麼返回Forbidden 否則建立連線

處理不同格式的訊息
上面的例子全部都是基於String的 Play內建支援Array[Byte]JsValue 比如:

import play.api.mvc._
import play.api.libs.json._
import play.api.Play.current
def socket = WebSocket.acceptWithActor[JsValue, JsValue] { request => out =>
  MyWebSocketActor.props(out)
}

也可以自定義需要處理的格式
比如下面這個例子i我們接收JSON訊息並且將其轉化為InEvent 返回的訊息轉化為OutEvent
第一件事是完成 InEvent和OutEvent的JSON轉換:(PS:能直接用format說明InEvent和OutEvent是case class)

import play.api.libs.json._  
implicit val inEventFormat = Json.format[InEvent]
implicit val outEventFormat = Json.format[OutEvent]

第二件事是建立FrameFormatter:

import play.api.mvc.WebSocket.FrameFormatter
implicit val inEventFrameFormatter = FrameFormatter.jsonFrame[InEvent]
implicit val outEventFrameFormatter = FrameFormatter.jsonFrame[OutEvent]  

最後可以用於WebSocket:

import play.api.mvc._
import play.api.Play.current
def socket = WebSocket.acceptWithActor[InEvent, OutEvent] { request => out =>
  MyWebSocketActor.props(out)
}  

使用Iteratees來處理WebSocket Actors對於處理訊息來說是個更好的抽象 Iteratee對於處理流來說是個更好的抽象

例子:

import play.api.mvc._
import play.api.libs.iteratee._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
def socket = WebSocket.using[String] { request =>
// Log events to the console
  val in = Iteratee.foreach[String](println).map { _ =>
    println("Disconnected")
  }
// Send a single 'Hello!' message
  val out = Enumerator("Hello!")
(in, out)
}

這種方式最後返回的是那兩個channel
in是Iteratee[A,Unit] 當接收到EOF時說明客戶端那邊關閉了socket
out是Enumerator[B] 當傳送EOF時說明服務端這邊關閉了Socket

第二個例子是忽視輸入 直接輸出後關閉:

import play.api.mvc._
import play.api.libs.iteratee._
def socket = WebSocket.using[String] { request =>
// Just ignore the input
  val in = Iteratee.ignore[String]
// Send a single 'Hello!' message and close
  val out = Enumerator("Hello!").andThen(Enumerator.eof)
(in, out)
}

Concurrent.broadcase可以用於廣播:

import play.api.mvc._
import play.api.libs.iteratee._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
def socket =  WebSocket.using[String] { request =>
// Concurrent.broadcast returns (Enumerator, Concurrent.Channel)
  val (out, channel) = Concurrent.broadcast[String]
// log the message to stdout and send response back to client
  val in = Iteratee.foreach[String] {
    msg => println(msg)
      // the Enumerator returned by Concurrent.broadcast subscribes to the channel and will
      // receive the pushed messages
      channel push("I received your message: " + msg)
  }
  (in,out)
}  

注意這個方法的返回值 第一個out用於返回時 第二個channel用於傳送訊息

相關文章