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模式即可逐片接收服务端推送的数据,核心逻辑是不等待完整响应返回,每次收到数据分片就做切分解析。
- 首先强持有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()
- 发起流式请求:
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() }
- 实现
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
相关产品推荐
相关产品推荐

