Swift - URLSessionUploadTask 在整个流被写入请求正文之前完成请求

Swift - URLSessionUploadTask finishes request before whole stream has been written to request body

我想达到的目标:

Swift 3.0 中,我目前正在尝试生成一个大的 XML 文件,我想通过 HTTP POST请求。因为这个XML文件会变得很大,所以我不想把它完全放在内存中,或者先写入磁盘,然后在发送到服务器时再逐行读取。

我已经实现了生成 XML 文件的 class,它可以写入 OutputStream。这样,无论该流指向磁盘上的文件、内存中的 Data 对象还是(希望如此)HTTP POST 请求的主体都无关紧要。

我打算做什么:

在搜索 URLSessionStream classes 及其同伙的(有点稀缺的)Swift 文档后,我决定使用 URLSession.uploadTask(withStreamedRequest) 任务。此请求需要通过以下委托方法之一传递 InputStream

urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)

在此回调中,我使用 Stream.getBoundStreams() 绑定 InputStreamOutputStream,然后将 OutputStream 传递给生成XML 和 return 来自委托方法的 InputStream。因此,委托方法如下所示:

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
{
    //Create the input and output stream and bind them, so that what the 
    //output stream writes ends up in the buffer of the input stream.
    var input: InputStream? = nil
    var output: OutputStream? = nil

    let bufferSize: Int = 1024
    Stream.getBoundStreams(withBufferSize: bufferSize, inputStream: &input, outputStream: &output)

    //This part is not really important for you, it starts the generation of 
    //the XML, which is written directly to the output stream.
    let converter = DatabaseConverterXml(prettyPrint: false)
    let type = ConverterTypeSynchronization(progressAlert: nil)

    type.convert(using: converter, writingTo: [Writable.Stream(output!)])
    {
        successfull in
        print("Conversion Complete! Successfull: \(successfull)" )
    }

    //The input stream is then handed over via the 
    //completion handler of the delegate method.
    completionHandler(input!)
}

我遇到的问题:

有时,生成 XML 的 class 可能需要一段时间才能将下一行写入 OutputStream。如果这种情况发生的时间太长,InputStream 可能会读取太多内容,以至于它实际上会清除整个缓冲区。当这种情况发生时,URLSession 框架(或者可能是 URLSessionUploadTask 本身)认为请求现在已完成,并且 "submits" 或 "finalizes" 它。然而,这是一个猜测,因为我不确定这些 classes 的内部工作原理(而且文档似乎对我帮助不大)。这会导致我的网络服务器收到不完整的 XML 文件和 return 500 Internal Server Error.

我的问题:

有什么方法可以阻止请求提前完成?最好是,我想在 type.convert 调用的回调中 "finalize" 输入流,因为我确信那时不会再发生写入(并且 OutputStream 在事实上关闭)。

奖励积分:

这是解决我要解决的问题的正确方法吗?有没有什么办法可以直接与写入 HTTP 正文的流进行交互?我对这个 URLSession 框架感到非常迷茫,我花了一天半的时间才走到这一步,所以非常感谢任何建议。我会请任何能帮我解决这个问题的人喝一两杯啤酒!

在此先感谢您的帮助!

编辑 1:

正如@dgatwood 所指出的,一些变量没有正确保留。我已进行以下更改以确保它们确实如此:

var mInput: InputStream? = nil
var mOutput: OutputStream? = nil
var mConverter: DatabaseConverterXml? = nil
var mType: ConverterTypeSynchronization? = nil

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
{
    //Create the input and output stream and bind them, so that what the 
    //output stream writes ends up in the buffer of the input stream.
    let bufferSize: Int = 1024
    Stream.getBoundStreams(withBufferSize: bufferSize, inputStream: &mInput, outputStream: &mOutput)

    //This part is not really important for you, it starts the generation of 
    //the XML, which is written directly to the output stream.
    mConverter = DatabaseConverterXml(prettyPrint: false)
    mType = ConverterTypeSynchronization(progressAlert: nil)

    mType.convert(using: mConverter, writingTo: [Writable.Stream(mOutput!)])
    {
        successfull in
        print("Conversion Complete! Successfull: \(successfull)" )
    }

    //The input stream is then handed over via the 
    //completion handler of the delegate method.
    completionHandler(mInput!)
}

在聊天 follow-up 之后的简短回答:

  • 执行写入的对象未被保留,因此当它被释放时,它会释放流,流将关闭。
  • 进行写入的对象,即使它正确地检查了 hasSpaceAvailable,也没有检测到短写入(因为可用的 space 少于正在写入的对象),因此每次写入都会丢失数据打电话。
  • 写入的对象最后没有关闭流。

顺便说一句,这些几乎是人们在使用 stream-based 网络 API 时常犯的错误。在使用相关的 Foundation-level 套接字 APIs.

时,我自己也犯过类似的错误

IMO,对于 API 只缓冲一个对象而不考虑其长度,然后发送一个 space 可用消息如果它在套接字中仍有空间会更有意义缓冲。这不需要对现有客户端进行任何更改,并且会减少很多麻烦……但我离题了。