使用NWConnection走蜂窝数据时isComplete始终不置真的问题
解决NWConnection接收HTTP响应时无法判断完成的问题
问题根源
NWConnection的isComplete参数标记的是TCP连接的关闭状态,而HTTP/1.1默认启用Keep-Alive机制,服务器返回响应后不会立即关闭TCP连接,因此这个参数永远不会变为true。你需要基于HTTP协议本身的规则来判断响应是否接收完成,而非依赖TCP层面的连接状态。
可行解决方案
针对常规HTTP接口的两种常见传输方式,分别处理:
1. 基于Content-Length头判断
如果HTTP响应头包含Content-Length字段,直接通过该字段的值确定响应体的总长度,当累计接收的数据长度达到该值时,即可判定接收完成。
2. 处理分块传输编码(Transfer-Encoding: chunked)
如果响应头包含Transfer-Encoding: chunked,说明响应体以分块形式传输:
- 每个分块开头是十六进制的长度值(后接
\r\n) - 随后是对应长度的分块数据(后接
\r\n) - 最后以长度为
0的分块(0\r\n\r\n)表示传输结束
修改后的代码示例
以下是结合HTTP协议解析的接收逻辑示例:
// 定义状态枚举,标记当前接收阶段 private enum ReceiveState { case waitingForHeaders case receivingBody(contentLength: Int) case receivingChunked(chunkRemaining: Int?) } private var receiveState: ReceiveState = .waitingForHeaders private var receivedData = Data() typealias dataCompletion = (Data?, Error?) -> Void func receive(withConnection connection: NWConnection, completion: @escaping dataCompletion) { connection.receive(minimumIncompleteLength: 1, maximumLength: Int.max) { [weak self] data, _, _, error in guard let self = self else { return } if let error = error { self.cancelConnection(connection) completion(nil, error) return } guard let data = data else { self.cancelConnection(connection) completion(nil, NSError(domain: "ReceiveError", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"])) return } self.receivedData.append(data) switch self.receiveState { case .waitingForHeaders: // 查找响应头和响应体的分隔符 \r\n\r\n guard let separatorIndex = self.receivedData.range(of: Data("\r\n\r\n".utf8))?.upperBound else { // 未拿到完整响应头,继续接收 self.receive(withConnection: connection, completion: completion) return } // 解析响应头 let headerData = self.receivedData[..<separatorIndex] let headers = self.parseHTTPHeaders(from: headerData) // 处理响应头,切换接收状态 if let contentLengthStr = headers["content-length"], let contentLength = Int(contentLengthStr) { // 剩余需要接收的体长度:总长度 - 已接收的体长度 let remainingBodyLength = contentLength - self.receivedData[separatorIndex...].count self.receiveState = .receivingBody(contentLength: contentLength) if remainingBodyLength <= 0 { // 已接收完整响应 let fullData = self.receivedData self.cancelConnection(connection) completion(fullData, nil) } else { // 继续接收剩余体数据 self.receive(withConnection: connection, completion: completion) } } else if headers["transfer-encoding"]?.lowercased() == "chunked" { self.receiveState = .receivingChunked(chunkRemaining: nil) // 开始解析分块数据 self.processChunkedData(completion: completion, connection: connection) } else { // 无法判断长度的情况(比如HTTP/1.0无Keep-Alive),可考虑超时或依赖连接关闭 self.cancelConnection(connection) completion(self.receivedData, nil) } case .receivingBody(let totalLength): if self.receivedData.count >= totalLength { // 已接收完整响应 let fullData = self.receivedData self.cancelConnection(connection) completion(fullData, nil) } else { // 继续接收 self.receive(withConnection: connection, completion: completion) } case .receivingChunked: self.processChunkedData(completion: completion, connection: connection) } } } // 解析HTTP响应头为键值对 private func parseHTTPHeaders(from data: Data) -> [String: String] { guard let headerString = String(data: data, encoding: .utf8) else { return [:] } let lines = headerString.components(separatedBy: "\r\n") var headers: [String: String] = [:] for line in lines where !line.isEmpty { let components = line.components(separatedBy: ": ", maxSplits: 1) guard components.count == 2 else { continue } let key = components[0].lowercased() headers[key] = components[1] } return headers } // 处理分块传输的数据 private func processChunkedData(completion: dataCompletion, connection: NWConnection) { while true { switch receiveState { case .receivingChunked(let chunkRemaining): if let remaining = chunkRemaining { if receivedData.count >= remaining { // 当前块接收完成,移除块结束的\r\n receivedData.removeLast(2) receiveState = .receivingChunked(chunkRemaining: nil) } else { // 继续接收当前块 receive(withConnection: connection, completion: completion) return } } else { // 查找块长度的结束符 \r\n guard let separatorIndex = receivedData.range(of: Data("\r\n".utf8))?.upperBound else { // 未拿到完整块长度,继续接收 receive(withConnection: connection, completion: completion) return } let chunkLengthData = receivedData[..<separatorIndex] guard let chunkLengthStr = String(data: chunkLengthData, encoding: .utf8), let chunkLength = Int(chunkLengthStr, radix: 16), chunkLength >= 0 else { // 块长度解析失败 cancelConnection(connection) completion(nil, NSError(domain: "ChunkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid chunk length"])) return } // 移除块长度和分隔符 receivedData.removeSubrange(...separatorIndex) if chunkLength == 0 { // 分块结束,移除最后的\r\n receivedData.removeLast(2) let fullData = receivedData cancelConnection(connection) completion(fullData, nil) return } else { // 需要接收当前块的数据,加上块结束的\r\n长度 receiveState = .receivingChunked(chunkRemaining: chunkLength + 2) } } default: return } } } // 假设你已有cancelConnection实现 private func cancelConnection(_ connection: NWConnection) { connection.cancel() }
额外提示
- 如果你的场景中同时存在HTTP/2协议,需要单独解析HTTP/2的帧结构(每个帧包含长度字段)
- 若不想手动解析HTTP协议,也可以考虑使用
URLSession并配置allowsCellularAccess = true,配合NWPathMonitor确保请求走蜂窝网络
内容的提问来源于stack exchange,提问作者MrAn3
相关产品推荐
相关产品推荐

