play2.x 官網筆記擷取+翻譯 第二章(完)
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.apply
和Action.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用於傳送訊息
相關文章
- play2.x 官網筆記擷取+翻譯 第一章(完)筆記
- influxdb官網文件翻譯UX
- substr擷取函式 筆記函式筆記
- 【翻譯】CoffeeScript 選譯筆記筆記
- Gremlin-官網介紹翻譯REM
- Hadoop官網翻譯之HDFS ArchitectureHadoop
- spring官網線上學習文件翻譯Spring
- Drill官網文件翻譯四Drill的效能
- android之Fragment(官網資料翻譯)AndroidFragment
- WPF 被 靈格斯翻譯官 取詞帶崩
- Hadoop官網翻譯之HDFS Users GuideHadoopGUIIDE
- hadoop官網翻譯第一天Hadoop
- Gradle入門(翻譯自Graddle官網)Gradle
- FreeLearning 安全譯文集翻譯完畢
- ARP協議:網路世界的地址翻譯官協議
- Elasticsearch官檔翻譯——2.6 升級Elasticsearch
- Drill官網文件翻譯二:Drill查詢的執行
- 爬取有道翻譯
- Kotlin 官方參考文件翻譯完畢Kotlin
- hadoop官網翻譯第三天Hadoop Cluster SetupHadoop
- Drill官網文件翻譯六:儲存外掛的註冊
- 有道雲詞典--翻譯/螢幕取詞翻譯
- 爬取必應翻譯
- 【工利其器】工具使用之(四)systrace篇(1)官網翻譯
- 【翻譯】蘋果官網的命名規範之 Naming Properties and Data Types蘋果
- javaSE第二章筆記Java筆記
- 【讀書筆記】代理模式翻譯成C++了筆記模式C++
- php字串擷取函式,支援中文擷取PHP字串函式
- 計算機網路學習筆記:第二章計算機網路筆記
- js擷取JS
- 擷取ip
- 字串擷取字串
- 日常筆記一:擷取富文字編輯器中的文字內容筆記
- 第二章(backup and recovery 筆記)筆記
- 【翻譯】使用PowerShell獲取網站執行時資料網站
- [翻譯] .NET 官宣跨平臺 UI 框架 MAUIUI框架
- 翻譯隨筆(隨時更新)
- hadoop官網翻譯之HDFS High Availability Using the Quorum Journal ManagerHadoopAI