文件

§串流 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


在這個文件中發現錯誤?此頁面的原始碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。