文件

§串流 HTTP 回應

§標準回應和內容長度標頭

自 HTTP 1.1 起,若要保持單一連線開啟以提供多個 HTTP 要求和回應,伺服器必須隨回應傳送適當的 Content-Length HTTP 標頭。

預設情況下,當您傳回簡單結果(例如)時,您不會指定 Content-Length 標頭:

public Result index() {
  return ok("Hello World");
}

當然,由於您傳送的內容是眾所周知的,Play 能夠為您計算內容大小並產生適當的標頭。

注意:對於基於文字的內容,這並不像看起來那麼簡單,因為 Content-Length 標頭必須根據用於將字元轉換為位元組的字元編碼來計算。

實際上,我們先前看到回應主體是使用 play.http.HttpEntity 指定的

public Result index() {
  return new Result(
      new ResponseHeader(200, Collections.emptyMap()),
      new HttpEntity.Strict(ByteString.fromString("Hello World"), Optional.of("text/plain")));
}

這表示為了正確計算 Content-Length 標頭,Play 必須使用整個內容並將其載入記憶體。

§傳送大量資料

如果將整個內容載入記憶體沒有問題,那麼大型資料集呢?假設我們想要傳回一個大型檔案給網頁客戶端。

讓我們先看看如何為檔案內容建立一個 Source[ByteString, _]

java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
java.nio.file.Path path = file.toPath();
Source<ByteString, ?> source = FileIO.fromPath(path);

現在看起來很簡單,對吧?讓我們使用這個串流的 HttpEntity 來指定回應主體

public Result index() {
  java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
  java.nio.file.Path path = file.toPath();
  Source<ByteString, ?> source = FileIO.fromPath(path);

  return new Result(
      new ResponseHeader(200, Collections.emptyMap()),
      new HttpEntity.Streamed(source, Optional.empty(), Optional.of("text/plain")));
}

實際上我們這裡有一個問題。由於我們沒有在串流實體中指定 Content-Length,Play 必須自行計算,而唯一的方法就是使用整個來源內容並將其載入記憶體,然後計算回應大小。

對於我們不想要完全載入記憶體的大型檔案來說,這是一個問題。因此,為了避免這種情況,我們必須自己指定 Content-Length 標頭。

public Result index() {
  java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
  java.nio.file.Path path = file.toPath();
  Source<ByteString, ?> source = FileIO.fromPath(path);

  Optional<Long> contentLength = null;
  try {
    contentLength = Optional.of(Files.size(path));
  } catch (IOException ioe) {
    throw new RuntimeException(ioe);
  }

  return new Result(
      new ResponseHeader(200, Collections.emptyMap()),
      new HttpEntity.Streamed(source, contentLength, Optional.of("text/plain")));
}

這樣,Play 將以延遲的方式使用主體來源,在每個資料區塊可用時將其複製到 HTTP 回應。

§提供檔案

當然,Play 提供了易於使用的輔助程式,用於提供本機檔案的常見任務

public Result index() {
  return ok(new java.io.File("/tmp/fileToServe.pdf"));
}

此輔助程式還會根據檔案名稱計算 Content-Type 標頭,並新增 Content-Disposition 標頭,以指定 Web 瀏覽器應如何處理此回應。預設會透過在 HTTP 回應中新增標頭 Content-Disposition: inline; filename=fileToServe.pdf 來以 inline 顯示此檔案。

您也可以提供自己的檔案名稱

public Result index() {
  return ok(new java.io.File("/tmp/fileToServe.pdf"), Optional.of("fileToServe.pdf"));
}

注意:如果計算出的標頭最後變成 完全 Content-Disposition: inline(在傳遞 null 作為檔案名稱時),Play 就不會傳送它,因為根據 RFC 6266 第 4.2 節,預設會以 inline 呈現內容。

如果您想提供此檔案 attachment

public Result index() {
  return ok(new java.io.File("/tmp/fileToServe.pdf"), /*inline = */ false);
}

現在您不必指定檔案名稱,因為 Web 瀏覽器不會嘗試下載它,而只會在 Web 瀏覽器視窗中顯示檔案內容。這對於 Web 瀏覽器原生支援的內容類型(例如文字、HTML 或影像)很有用。

§區塊回應

目前,這對於串流檔案內容很有效,因為我們可以在串流檔案內容之前計算內容長度。但是對於沒有可用內容大小的動態計算內容呢?

對於這種類型的回應,我們必須使用區塊傳輸編碼

區塊傳輸編碼是 HTTP 1.1 版本中的資料傳輸機制,其中 Web 伺服器以一系列區塊提供內容。這會使用 Transfer-Encoding HTTP 回應標頭,而不是協定原本需要的 Content-Length 標頭。由於不使用 Content-Length 標頭,因此伺服器不需要在開始向用戶端(通常是 Web 瀏覽器)傳送回應之前就知道內容的長度。Web 伺服器可以在知道該內容的總大小之前開始傳送動態產生內容的回應。

每個區塊的大小會在區塊本身傳送之前傳送,以便客戶端可以得知何時已完成接收該區塊的資料。資料傳輸會以長度為零的最後一個區塊終止。

https://en.wikipedia.org/wiki/Chunked_transfer_encoding

優點是我們可以即時提供資料,表示我們會在資料可用時立即傳送資料區塊。缺點是,由於網頁瀏覽器不知道內容大小,因此無法顯示正確的下載進度條。

假設我們在某個地方有一個服務,提供計算某些資料的動態 InputStream。我們可以要求 Play 使用區塊回應直接串流此內容

public Result index() {
  InputStream is = getDynamicStreamSomewhere();
  return ok(is);
}

您也可以設定自己的區塊回應建構器

public Result index() {
  // Prepare a chunked text stream
  Source<ByteString, ?> source =
      Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
          .mapMaterializedValue(
              sourceActor -> {
                sourceActor.tell(ByteString.fromString("kiki"), null);
                sourceActor.tell(ByteString.fromString("foo"), null);
                sourceActor.tell(ByteString.fromString("bar"), null);
                sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
                return NotUsed.getInstance();
              });
  // Serves this stream with 200 OK
  return ok().chunked(source);
}

方法 Source.actorRef 會建立一個 Pekko Streams Source,具現化為 ActorRef。然後,您可以透過傳送訊息給 actor,將元素發佈到串流中。另一種方法是建立一個延伸 ActorPublisher 的 actor,並使用 Stream.actorPublisher 方法來建立它。

我們可以檢查伺服器傳送的 HTTP 回應

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

4
kiki
3
foo
3
bar
0

我們會取得三個區塊和一個關閉回應的最後一個空區塊。

如需有關使用 Pekko Streams 的更多資訊,您可以參閱 Pekko Streams 文件

下一步:Comet


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