You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端如何使用ndjson打开HTTP流 两种实现均报错如何解决

方案失效原因

  • 带completion回调的普通dataTask默认逻辑是等待服务端返回全部响应数据、连接关闭后才触发回调,长连接流式推送场景下连接持续保持,回调永远不会执行,分片接收数据不属于该API的设计范畴。
  • URLSessionStreamTask是面向裸TCP/UDP流的底层工具,不内置HTTP协议实现:直接传入完整URL作为主机名属于参数错误,同时未手动构造合法HTTP请求报文发送给服务端,服务端无法识别请求直接断开,才会抛出Connection 0: encountered error(12:1)错误。

正确实现方案

iOS端接收HTTP流式ndjson响应不需要使用底层Stream API,直接通过URLSession的delegate模式即可逐片接收服务端推送的数据,核心逻辑是不等待完整响应返回,每次收到数据分片就做切分解析。

  1. 首先强持有URLSession实例,避免被系统回收导致连接断开,同时预留数据缓存处理TCP分片问题:
// 注意:session必须被当前类强持有,不能定义为局部变量
private lazy var streamSession: URLSession = {
    let config = URLSessionConfiguration.default
    config.timeoutIntervalForRequest = .infinity
    config.timeoutIntervalForResource = 86400 // 按业务需求调整最长连接时长
    return URLSession(configuration: config, delegate: self, delegateQueue: .main)
}()
// 缓存未拼接完成的半截数据
private var receivedDataBuffer = Data()
  1. 发起流式请求:
func startNdjsonStream() throws {
    let keyID = try keyAdapter.getKeyID(for: .signHash)
    let url = baseURL.appendingPathComponent("/v2/path/\(keyID)")
    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = "GET"
    urlRequest.setValue("application/x-ndjson", forHTTPHeaderField: "Accept")
    let task = streamSession.dataTask(with: urlRequest)
    task.resume()
}
  1. 实现URLSessionDataDelegate相关回调处理流式数据:
extension YourClass: URLSessionDataDelegate {
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        // 校验响应合法性
        guard let httpResp = response as? HTTPURLResponse,
              httpResp.statusCode == 200,
              httpResp.mimeType == "application/x-ndjson" else {
            completionHandler(.cancel)
            return
        }
        // 核心配置:允许持续接收数据分片,不等待响应完成
        completionHandler(.allow)
        receivedDataBuffer.removeAll()
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        receivedDataBuffer.append(data)
        // 按ndjson规则,用\n分隔每条独立JSON
        while let newlinePos = receivedDataBuffer.firstIndex(of: 0x0A) { // 0x0A为换行符\n的ASCII值
            let lineData = receivedDataBuffer[receivedDataBuffer.startIndex..<newlinePos]
            // 移除已处理的完整行
            receivedDataBuffer.removeSubrange(receivedDataBuffer.startIndex...newlinePos)
            
            // 按需处理单行数据,可做\r去除、JSON反序列化等操作
            do {
                // let model = try JSONDecoder().decode(YourModel.self, from: lineData)
                // 执行业务逻辑处理单条推送数据
            } catch {
                // 单条数据解析失败按业务需求处理,无需直接断开连接
            }
        }
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        receivedDataBuffer.removeAll()
        if let error = error {
            Logger.shared.log(level: .error, "流连接出错: \(error.localizedDescription)")
            // 按需实现自动重连逻辑
        } else {
            Logger.shared.log(level: .info, "流连接被服务端关闭")
        }
    }
}

注意事项

  • 不要用URLSessionStreamTask实现HTTP流:这个类是为自定义应用层协议准备的,用它需要手动实现TCP握手、TLS握手、HTTP请求报文构造、响应解析全流程,没有实际使用价值。
  • 必须做本地数据缓存:TCP传输存在分片机制,一次回调收到的数据可能包含多条JSON,也可能只包含半条JSON,必须缓存后按换行符切分,不能拿到data就直接反序列化。
  • 该实现同时兼容HTTP/1.1 chunked传输、HTTP/2流式响应场景,不需要额外适配。如果需要避免连接被中间节点(CDN、运营商)因空闲超时断开,可按业务需求实现应用层心跳逻辑,或调整超时配置。

内容的提问来源于stack exchange,提问作者Lachtan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:03:27