§串流 HTTP 回應
§標準回應和 Content-Length
標頭
自 HTTP 1.1 以來,為了讓單一連線保持開啟狀態以提供多個 HTTP 要求和回應,伺服器必須連同回應傳送適當的 Content-Length
HTTP 標頭。
預設情況下,當您傳回簡單結果時,您不會指定 Content-Length
標頭,例如
def index = Action {
Ok("Hello World")
}
當然,由於您傳送的內容是眾所周知的,因此 Play 能夠為您計算內容大小並產生適當的標頭。
注意:對於基於文字的內容,這不像看起來那麼簡單,因為
Content-Length
標頭必須根據用於將字元轉換為位元的字元編碼進行計算。
實際上,我們之前看到回應主體是使用 play.api.http.HttpEntity
指定的
def action = Action {
Result(
header = ResponseHeader(200, Map.empty),
body = HttpEntity.Strict(ByteString("Hello world"), Some("text/plain"))
)
}
這表示要正確計算 Content-Length
標頭,Play 必須使用整個內容並將其載入記憶體。
§傳送大量資料
如果將整個內容載入記憶體不是問題,那麼大型資料集呢?假設我們要傳回一個大型檔案給網路用戶端。
讓我們先看看如何為檔案內容建立 Source[ByteString, _]
val file = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)
現在看起來很簡單,對吧?我們只要使用這個串流 HttpEntity 來指定回應主體即可
def streamed = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)
Result(
header = ResponseHeader(200, Map.empty),
body = HttpEntity.Streamed(source, None, Some("application/pdf"))
)
}
實際上我們這裡有一個問題。由於我們沒有在串流實體中指定 Content-Length
,Play 必須自行計算,而唯一的方法就是使用整個來源內容並將其載入記憶體,然後計算回應大小。
對於我們不想要完全載入記憶體的大型檔案來說,這是一個問題。因此,為了避免這種情況,我們只需要自己指定 Content-Length
標頭。
def streamedWithContentLength = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)
val contentLength = Some(Files.size(file.toPath))
Result(
header = ResponseHeader(200, Map.empty),
body = HttpEntity.Streamed(source, contentLength, Some("application/pdf"))
)
}
這樣,Play 將以延遲的方式使用主體來源,在每個資料區塊可用時將其複製到 HTTP 回應中。
§提供檔案
當然,Play 提供了易於使用的輔助程式,用於提供本機檔案的常見任務
def file = Action {
Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}
這個輔助程式還會從檔案名稱計算 Content-Type
標頭,並加入 Content-Disposition
標頭來指定網路瀏覽器應如何處理這個回應。預設是透過在 HTTP 回應中加入標頭 Content-Disposition: inline; filename=fileToServe.pdf
來內嵌顯示這個檔案。
你也可以提供自己的檔案名稱
def fileWithName = Action {
Ok.sendFile(
content = new java.io.File("/tmp/fileToServe.pdf"),
fileName = _ => Some("termsOfService.pdf")
)
}
注意:如果計算出來的標頭最後變成完全
Content-Disposition: inline
(當傳回null
作為檔案名稱時:fileName = _ => null
),Play 將不會傳送它,因為根據 RFC 6266 第 4.2 節,內嵌呈現內容是預設值。
如果您想提供這個檔案的 附件
def fileAttachment = Action {
Ok.sendFile(
content = new java.io.File("/tmp/fileToServe.pdf"),
inline = false
)
}
現在您不必指定檔案名稱,因為網頁瀏覽器不會嘗試下載它,而只會在網頁瀏覽器視窗中顯示檔案內容。這對網頁瀏覽器原生支援的內容類型很有用,例如文字、HTML 或圖片。
§分塊回應
目前,它與串流檔案內容搭配得很好,因為我們可以在串流之前計算內容長度。但是對於動態計算的內容,沒有內容大小可用,該怎麼辦?
對於這種回應,我們必須使用分塊傳輸編碼。
分塊傳輸編碼是超文字傳輸協定 (HTTP) 1.1 版中的一種資料傳輸機制,其中網頁伺服器以一系列分塊提供內容。它使用
Transfer-Encoding
HTTP 回應標頭,而不是協定原本需要的Content-Length
標頭。由於不使用Content-Length
標頭,伺服器在開始傳送回應給客戶端(通常是網頁瀏覽器)之前,不需要知道內容的長度。網頁伺服器可以在知道該內容的總大小之前,開始傳送動態產生內容的回應。每個分塊的大小會在分塊本身之前傳送,以便客戶端知道何時已完成接收該分塊的資料。資料傳輸會由長度為零的最後一個分塊終止。
https://zh.wikipedia.org/wiki/%E5%88%86%E5%9D%97%E5%82%B3%E8%BC%B8%E7%BC%96%E7%A0%81
優點是我們可以即時提供資料,表示我們會在資料可用時立即傳送資料區塊。缺點是,由於網頁瀏覽器不知道內容大小,因此無法顯示適當的下載進度條。
假設我們某處有提供動態 InputStream
計算某些資料的服務。首先,我們必須為此串流建立 Source
val data = getDataStream
val dataContent: Source[ByteString, _] = StreamConverters.fromInputStream(() => data)
我們現在可以使用 Ok.chunked
串流這些資料
def chunked = Action {
val data = getDataStream
val dataContent: Source[ByteString, _] = StreamConverters.fromInputStream(() => data)
Ok.chunked(dataContent)
}
當然,我們可以使用任何 Source
來指定區塊資料
def chunkedFromSource = Action {
val source = Source.apply(List("kiki", "foo", "bar"))
Ok.chunked(source)
}
我們可以檢查伺服器傳送的 HTTP 回應
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
4
kiki
3
foo
3
bar
0
我們會取得三個區塊,接著是一個最後的空白區塊,用來關閉回應。
下一步:Comet
在這個文件中發現錯誤?此頁面的原始碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。