【问题标题】:How to use URLSessionStreamTask with URLSession for chunked-encoding transfer如何使用 URLSessionStreamTask 和 URLSession 进行分块编码传输
【发布时间】:2017-11-19 23:53:07
【问题描述】:

我正在尝试连接到 Twitter 流 API 端点。看起来URLSession 支持通过URLSessionStreamTask 进行流式传输,但是我不知道如何使用 API。我也找不到任何示例代码。

我尝试测试以下内容,但没有记录网络流量:

let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let stream = session.streamTask(withHostName: "https://stream.twitter.com/1.1/statuses/sample.json", port: 22)
stream.startSecureConnection()
stream.readData(ofMinLength: 0, maxLength: 100000, timeout: 60, completionHandler: { (data, bool, error) in
   print("bool = \(bool)")
   print("error = \(String(describing: error))")
})
stream.resume()

我还实现了委托方法(包括URLSessionStreamDelegate),但它们没有被调用。

如果有人代码发布了如何为来自流端点的chunked 响应打开持久连接的示例,那将非常有帮助。另外,我正在寻找不涉及第三方库的解决方案。一个类似于 https://stackoverflow.com/a/9473787/5897233 但更新为等效的 URLSession 的响应是理想的。

注意:上面的示例代码中省略了授权信息。

【问题讨论】:

  • 你解决过这个问题吗?自己找了好几个小时。文档真的很少。
  • @Ryan 刚刚发布了答案!

标签: ios swift network-programming nsurlsession chunked


【解决方案1】:

收到了 Apple 的 Quinn “The Eskimo” 提供的大量信息。

唉,你在这里搞错了。 URLSessionStreamTask 用于处理裸 TCP(或 TLS over TCP)连接,顶部没有 HTTP 框架。您可以将其视为高级别的 BSD 套接字 API。

分块传输编码是 HTTP 的一部分,因此所有其他 URLSession 任务类型(数据任务、上传任务、下载任务)都支持。你不需要做任何特别的事情来启用它。分块传输编码是 HTTP 1.1 标准的强制性部分,因此始终处于启用状态。

但是,您可以选择如何接收返回的数据。如果您使用 URLSession 便利 API(dataTask(with:completionHandler:) 等),URLSession 将缓冲所有传入的数据,然后将其以一个大的 Data 值传递给您的完成处理程序。这在许多情况下很方便,但不适用于流式资源。在这种情况下,您需要使用基于 URLSession 委托的 API(dataTask(with:) 等),它会在数据块到达时调用 urlSession(_:dataTask:didReceive:) 会话委托方法。

至于我正在测试的特定端点,发现了以下内容: 如果客户端向其发送流式请求,服务器似乎仅启用其流式响应(分块传输编码)。这有点奇怪,而且 HTTP 规范绝对不需要。

幸运的是,可以强制 URLSession 发送流式请求:

  1. 使用uploadTask(withStreamedRequest:)创建您的任务

  2. 实现 urlSession(_:task:needNewBodyStream:) 委托方法以返回输入流,读取时返回请求正文

  3. 利润!

我附上了一些测试代码来展示这一点。在这种情况下,它使用一对绑定的流,将输入流传递给请求(按照上面的步骤 2)并保持输出流。

如果您想实际发送数据作为请求正文的一部分,您可以通过写入输出流来实现。

class NetworkManager : NSObject, URLSessionDataDelegate {

static var shared = NetworkManager()

private var session: URLSession! = nil

override init() {
    super.init()
    let config = URLSessionConfiguration.default
    config.requestCachePolicy = .reloadIgnoringLocalCacheData
    self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
}

private var streamingTask: URLSessionDataTask? = nil

var isStreaming: Bool { return self.streamingTask != nil }

func startStreaming() {
    precondition( !self.isStreaming )

    let url = URL(string: "ENTER STREAMING URL HERE")!
    let request = URLRequest(url: url)
    let task = self.session.uploadTask(withStreamedRequest: request)
    self.streamingTask = task
    task.resume()
}

func stopStreaming() {
    guard let task = self.streamingTask else {
        return
    }
    self.streamingTask = nil
    task.cancel()
    self.closeStream()
}

var outputStream: OutputStream? = nil

private func closeStream() {
    if let stream = self.outputStream {
        stream.close()
        self.outputStream = nil
    }
}

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
    self.closeStream()

    var inStream: InputStream? = nil
    var outStream: OutputStream? = nil
    Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
    self.outputStream = outStream

    completionHandler(inStream)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    NSLog("task data: %@", data as NSData)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error as NSError? {
        NSLog("task error: %@ / %d", error.domain, error.code)
    } else {
        NSLog("task complete")
    }
}
}

您可以从任何地方调用网络代码,例如:

class MainViewController : UITableViewController {

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if NetworkManager.shared.isStreaming {  
       NetworkManager.shared.stopStreaming() 
    } else {
       NetworkManager.shared.startStreaming() 
    }
    self.tableView.deselectRow(at: indexPath, animated: true)
}
}

希望这会有所帮助。

【讨论】:

  • 我偶然发现了这个非常有用的答案,虽然当我的应用程序处于前台时它工作得很好,但当我转换到后台时它不起作用。基本上,我与请求事件流的 RESTful 端点进行通信。事件流打开并忠实地传递事件,直到应用程序转换到后台。即使我的流仍在运行,我也不再收到事件。我尝试了多种方法,围绕创建具有后台配置的 URLSession。有什么一般的想法吗?
  • 虽然这种方法在 iOS 12 中非常适合我,但它在 iOS 13 中不再适用。我收到错误消息“GET 方法不能有正文”。但是查看请求对象,httpBody 字段为 nil,这对我来说毫无意义。有什么想法吗?
  • GET 请求不应该有正文。使用 PUT 或 POST 方法发出具有正文的请求。
  • QBit,不需要uploadTask,改成let task = self.session.dataTask(with: request)就可以了。
  • 非常感谢您!我花了几个小时查看示例,但不明白为什么我的委托方法没有被调用。原来我使用的是URLSessionDelegate 而不是URLSessionDataDelegate,正如您在此处的回答中提到的那样。四个字母让一切变得不同 :) 再次感谢。
猜你喜欢
  • 2017-07-22
  • 2012-02-18
  • 2012-01-26
  • 2011-02-09
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 2014-07-01
相关资源
最近更新 更多