【问题标题】:How to write a file downloaded with SwiftNIO AsyncHTTPClient to the filesystem?如何将使用 SwiftNIO AsyncHTTPClient 下载的文件写入文件系统?
【发布时间】:2026-02-12 04:00:01
【问题描述】:

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

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

【问题讨论】:

    标签: swift http asynchronous file-io swift-nio


    【解决方案1】:

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

    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 的文件不会消耗超过 5MB 的 RAM。

    【讨论】: