1

我想使用基于SwiftNIO的AsyncHTTPClient库下载一个大文件(数百兆字节) 。我希望将此文件流式传输到文件系统,同时消耗尽可能少的 RAM(理想情况下,它不应该将整个文件保留在 RAM 中),并且还能够通过显示的简单输出报告下载进度完成百分比。print

据我了解,我需要实现一个HTTPClientResponseDelegate,但我应该使用什么确切的 API 来进行文件写入?文件写入可以被阻塞,同时仍然允许 HTTP 客户端进行吗?在这种情况下,委托代码会如何?

4

1 回答 1

0

事实证明,HTTPClientResponseDelegate它允许在其函数中返回一个未来,以允许它正确处理背压。将此方法与NonBlockingFileIOand结合使用NIOFileHandle,在下载文件时将文件写入磁盘并提供进度报告的委托如下所示:

import AsyncHTTPClient
import NIO
import NIOHTTP1

final class FileDownloadDelegate: HTTPClientResponseDelegate {
  typealias Response = (totalBytes: Int?, receivedBytes: Int)

  private var totalBytes: Int?
  private var receivedBytes = 0

  private let handle: NIOFileHandle
  private let io: NonBlockingFileIO
  private let reportProgress: (_ totalBytes: Int?, _ receivedBytes: Int) -> ()

  private var writeFuture: EventLoopFuture<()>?

  init(
    path: String,
    reportProgress: @escaping (_ totalBytes: Int?, _ receivedBytes: Int) -> ()
  ) throws {
    handle = try NIOFileHandle(path: path, mode: .write, flags: .allowFileCreation())
    let pool = NIOThreadPool(numberOfThreads: 1)
    pool.start()
    io = NonBlockingFileIO(threadPool: pool)

    self.reportProgress = reportProgress
  }

  func didReceiveHead(
    task: HTTPClient.Task<Response>,
    _ head: HTTPResponseHead
  ) -> EventLoopFuture<()> {
    if let totalBytesString = head.headers.first(name: "Content-Length"),
      let totalBytes = Int(totalBytesString) {
      self.totalBytes = totalBytes
    }

    return task.eventLoop.makeSucceededFuture(())
  }

  func didReceiveBodyPart(
    task: HTTPClient.Task<Response>,
    _ buffer: ByteBuffer
  ) -> EventLoopFuture<()> {
    receivedBytes += buffer.readableBytes
    reportProgress(totalBytes, receivedBytes)

    let writeFuture = io.write(fileHandle: handle, buffer: buffer, eventLoop: task.eventLoop)
    self.writeFuture = writeFuture
    return writeFuture
  }

  func didFinishRequest(task: HTTPClient.Task<Response>) throws -> Response {
    writeFuture?.whenComplete { [weak self] _ in
      try? self?.handle.close()
      self?.writeFuture = nil
    }
    return (totalBytes, receivedBytes)
  }
}

使用此代码,下载和写入文件的过程对于大约 600MB 的下载文件消耗的 RAM 不会超过 5MB。

于 2020-06-24T16:38:45.383 回答