将 NWConnection 用于长时间运行的 TCP 套接字的正确方法

Posted

技术标签:

【中文标题】将 NWConnection 用于长时间运行的 TCP 套接字的正确方法【英文标题】:Correct way to use NWConnection for long-running TCP socket 【发布时间】:2019-03-04 04:12:10 【问题描述】:

我整天都在与 NWConnection 斗争,以便在长时间运行的 TCP 套接字上接收数据。由于缺乏文档,在对自己造成以下错误后,我终于让它工作了:

    数据不完整(由于只调用receive一次) 无序获取 TCP 数据(由于从计时器“轮询”接收...导致多个同时关闭等待获取数据)。 遭受无限循环(由于在接收后重新启动接收而没有检查“isComplete”布尔值——一旦套接字从另一端终止,这是....糟糕......非常糟糕)。

我所学的总结:

    一旦您处于 .ready 状态,您就可以调用 receive...一次且仅一次 收到一些数据后,您可以再次调用 receive...但前提是您仍处于 .ready 状态并且 isComplete 为 false。

这是我的代码。我认为这是对的。但如果有错误请告诉我:

    queue = DispatchQueue(label: "hostname", attributes: .concurrent)
    let serverEndpoint = NWEndpoint.Host(hostname)
    guard let portEndpoint = NWEndpoint.Port(rawValue: port) else  return nil 
    connection = NWConnection(host: serverEndpoint, port: portEndpoint, using: .tcp)
    connection.stateUpdateHandler =  [weak self] (newState) in
        switch newState 
        case .ready:
            debugPrint("TcpReader.ready to send")
            self?.receive()
        case .failed(let error):
            debugPrint("TcpReader.client failed with error \(error)")
        case .setup:
            debugPrint("TcpReader.setup")
        case .waiting(_):
            debugPrint("TcpReader.waiting")
        case .preparing:
            debugPrint("TcpReader.preparing")
        case .cancelled:
            debugPrint("TcpReader.cancelled")
        
    

func receive()   
    connection.receive(minimumIncompleteLength: 1, maximumLength: 8192)  (content, context, isComplete, error) in
        debugPrint("\(Date()) TcpReader: got a message \(String(describing: content?.count)) bytes")
        if let content = content 
            self.delegate.gotData(data: content, from: self.hostname, port: self.port)
        
        if self.connection.state == .ready && isComplete == false 
            self.receive()
        
    

【问题讨论】:

我希望今天早上能找到这篇文章。我正在努力解决如果我使用 connection.send 发送多个数据位然后连接接收将数据组合在一起的问题。我应该将其视为仅在网络上发生的事情,还是应该限制我的发送,还是应该以不同的方式发送? 我无法回答这个问题,但这听起来是一个很好的问题,尤其是在您包含代码的情况下。我希望看到更多 NWconnection 代码示例。 所以,这表明我有点以错误的方式使用它(AFAIK)。我把连接看作是你打开的管道,然后不断地把东西放进去。当我将其视为 NWConnection 是用于发送单个事物并在该单个事物之后关闭时,一切都开始正常工作。 如果要多次连接可以处理newConnectionHandler,在服务器上重启NWListener和NWConnection。 不需要定时器。您应该处理NWConnection.receiveMessage 来获取消息并调用receiveNextMessage() 来获取下一个。 【参考方案1】:

我认为您可以多次使用短时间连接。例如,客户端连接到主机并要求主机做某事,然后告诉主机关闭连接。主机切换到等待模式以准备新的连接。见下图。

当客户端在特定时间内没有向主机发送关闭连接或应答事件时,您应该有连接计时器来关闭打开的连接。

【讨论】:

【参考方案2】:

在一个长时间运行的 TCP 套接字上,你应该实现自定义的心跳来监控连接状态是活动的还是断开的。

心跳可以作为消息或加密数据发送,通常根据服务器规范文件来实现。

下面作为示例概念代码来解释流程以供参考(没有网络数据包内容处理程序)。

我不能保证这种方法是常见且正确的,但这对我的项目有效。

import Network

class NetworkService 

    lazy var heartbeatTimeoutTask: DispatchWorkItem = 
        return DispatchWorkItem  self.handleHeartbeatTimeOut() 
    ()

    lazy var connection: NWConnection = 
        // Create the connection
        let connection = NWConnection(host: "x.x.x.x", port: 1234, using: self.parames)
        connection.stateUpdateHandler = self.listenStateUpdate(to:)
        return connection
    ()
    
    lazy var parames: NWParameters = 
        let parames = NWParameters(tls: nil, tcp: self.tcpOptions)
        if let isOption = parames.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options 
            isOption.version = .v4
        
        parames.preferNoProxies = true
        parames.expiredDNSBehavior = .allow
        parames.multipathServiceType = .interactive
        parames.serviceClass = .background
        return parames
    ()
    
    lazy var tcpOptions: NWProtocolTCP.Options = 
        let options = NWProtocolTCP.Options()
        options.enableFastOpen = true // Enable TCP Fast Open (TFO)
        options.connectionTimeout = 5 // connection timed out
        return options
    ()
    
    let queue = DispatchQueue(label: "hostname", attributes: .concurrent)
    
    private func listenStateUpdate(to state: NWConnection.State) 
        // Set the state update handler
        switch state 
        case .setup:
            // init state
            debugPrint("The connection has been initialized but not started.")
        case .waiting(let error):
            debugPrint("The connection is waiting for a network path change with: \(error)")
            self.disconnect()
        case .preparing:
            debugPrint("The connection in the process of being established.")
        case .ready:
            // Handle connection established
            // this means that the handshake is finished
            debugPrint("The connection is established, and ready to send and receive data.")
            self.receiveData()
            self.sendHeartbeat()
        case .failed(let error):
            debugPrint("The connection has disconnected or encountered an: \(error)")
            self.disconnect()
        case .cancelled:
            debugPrint("The connection has been canceled.")
        default:
            break
        
    
    
    // MARK: - Socket I/O
    func connect() 
        // Start the connection
        self.connection.start(queue: self.queue)
    
    
    func disconnect() 
        // Stop the connection
        self.connection.stateUpdateHandler = nil
        self.connection.cancel()
    
    
    private func sendPacket() 
        var packet: Data? // do something for heartbeat packet
        self.connection.send(content: packet, completion: .contentProcessed( (error) in
            if let err = error 
                // Handle error in sending
                debugPrint("encounter an error with: \(err) after send Packet")
             else 
                // Send has been processed
            
        ))
    
    
    private func receiveData() 
        self.connection.receive(minimumIncompleteLength: 1, maximumLength: 8192)  [weak self] (data, context, isComplete, error) in
            guard let weakSelf = self else  return 
            if weakSelf.connection.state == .ready && isComplete == false, var data = data, !data.isEmpty 
                // do something for detect heart packet
                weakSelf.parseHeartBeat(&data)
            
        
    
    
    // MARK: - Heartbeat
    private func sendHeartbeat() 
        // sendHeartbeatPacket
        self.sendPacket()
        // trigger timeout mission if the server no response corresponding packet within 5 second
        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5.0, execute: self.heartbeatTimeoutTask)
    
    
    private func handleHeartbeatTimeOut() 
        // this's sample time out mission, you can customize this chunk
        self.heartbeatTimeoutTask.cancel()
        self.disconnect()
    
    
    private func parseHeartBeat(_ heartbeatData: inout Data) 
        // do something for parse heartbeat
        
        // cancel heartbeat timeout after parse packet success
        self.heartbeatTimeoutTask.cancel()
        
        // send heartbeat for monitor server after computing 15 second
        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 15.0) 
            self.sendHeartbeat()
        
    


【讨论】:

以上是关于将 NWConnection 用于长时间运行的 TCP 套接字的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

Android AsyncTask 用于长时间运行的操作

用于长时间运行代码的 gevent 探查器

无法在 iOS 设备上发送 UDP 广播 - NWConnection

长时间运行的Django进程可行吗?

用于长时间运行过程的 Spring SSE

带有异步或长时间运行任务的 UndoManager