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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 15:31:11