【问题标题】:Swift NWListener listen, cancel, and relisten successfully?Swift NWListener 成功监听、取消和重新监听?
【发布时间】:2020-11-20 12:52:22
【问题描述】:

我有一个应用程序,我应该有一个 websocket,仅当应用程序处于前台时才侦听我已经利用生命周期通知并调用 start() 并相应地停止。该应用程序正常工作,直到应用程序回到前台,此时我收到许多警告和错误。

class SwiftWebSocketServer {
    let port: NWEndpoint.Port
    var listener: NWListener?
    var listenerState: NWListener.State
    let eventHandler:()->Void
    var connection: ServerConnection?

    init(port: UInt16, handler:@escaping ()->Void) {
        self.port = NWEndpoint.Port(rawValue: port)!
        listenerState = .cancelled
        self.eventHandler = handler
        let parameters = NWParameters(tls: nil)
        parameters.allowLocalEndpointReuse = true
        parameters.includePeerToPeer = true
        let wsOptions = NWProtocolWebSocket.Options()
        wsOptions.autoReplyPing = true
        parameters.defaultProtocolStack.applicationProtocols.insert(wsOptions, at: 0)
        do {
           listener = try NWListener(using: parameters, on: self.port)
           listener!.stateUpdateHandler = self.stateDidChange(to:)
           listener!.newConnectionHandler = self.didAccept(nwConnection:)
        } catch {
            print(#function, error)
        }
    }

    func start() throws {
        print("Server starting...")
        listener!.stateUpdateHandler = self.stateDidChange(to:)
        listener!.newConnectionHandler = self.didAccept(nwConnection:)
        listener!.start(queue: .main)
        print("Server started.")
        eventHandler()
    }

    func stop() {
        self.listener!.stateUpdateHandler = nil
        self.listener!.newConnectionHandler = nil
        self.listener!.cancel()
        print("Server cancelled")
        connection?.stop()
        connection?.didStopCallback = nil
        connection = nil
        eventHandler()
    }
    func stateDidChange(to newState: NWListener.State) {
        print(#function, newState)
        switch newState {
        case .ready:
            print("Server ready.")
        case .failed(let error):
            print("Server failure, error: \(error.localizedDescription)")
            exit(EXIT_FAILURE)
        default:
            break
        }
        listenerState = newState
        eventHandler()
    }
}

日志:

Server starting...
Server started
App moved to background!
Server cancelled
App moved to foreground!
Server starting...
2020-07-30 13:45:48.269100-0400 rfa-ios-native[584:10739501] [] nw_listener_set_queue Error in client: nw_listener_set_queue called after nw_listener_start
2020-07-30 13:45:48.271526-0400 rfa-ios-native[584:10739501] [] nw_listener_set_queue Error in client: nw_listener_set_queue called after nw_listener_start, dumping backtrace:
    [arm64] libnetcore-1880.40.26
0   libnetwork.dylib                    0x00000001c5cb9ae8 __nw_create_backtrace_string + 116
1   libnetwork.dylib                    0x00000001c5bd8c3c nw_listener_set_queue + 224
2   libswiftNetwork.dylib               0x00000001f86c737c $s7Network10NWListenerC5start5queueySo012OS_dispatch_D0C_tF + 52
3   rfa-ios-native                      0x0000000104f64ec4 $s14rfa_ios_native20SwiftWebSocketServerC5startyyKF + 432
4   rfa-ios-native                      0x0000000104f34468 $s14rfa_ios_native14ViewControllerC20appMovedToForegroundyyF + 296
5   rfa-ios-native                      0x0000000104f34634 $s14rfa_ios_native14ViewControllerC20appMovedToForegroundyyFTo + 48
...
Server started.

即使在消息和堆栈跟踪之外,侦听器也没有在侦听。我需要做什么才能取消监听并在同一个端口上重新监听?

【问题讨论】:

  • 同理。你找到解决方法了吗?文档说 'cancel()' 是异步的,所以取消何时完成尚不清楚。我们需要找到一种方法。

标签: swift afnetworking


【解决方案1】:

我终于找到了适用于所有情况的更好解决方案,但我需要提前告诉您,这需要等待时间。等待时间的范围从没有活动连接时的几毫秒到 30 秒(准确地说,在我的情况下最多 26 秒)。如果这太多了,您可以放心地跳过这篇文章,否则 - 继续阅读。

需要在您的自定义 TCP 侦听器类(下例中的 TcpListener)中进行更改。这是 init 在我的例子中的样子:

class TcpListener {
...
let maxStartAttempts = 60 // just in case, 30 would suffice in the most cases

init(port: UInt16, onRead: @escaping (String, Int) -> Void) {
    
        self.onRead = onRead // some consumer's function to read received data
        self.initPort(port)
        self.port = NWEndpoint.Port(rawValue: port)!
    
}

func initPort(_ p: UInt16) {
        let opts = NWProtocolTCP.Options()
        opts.persistTimeout = 0 // this one reduces waiting time significantly when there is no open connections
        opts.enableKeepalive = true // this one reduces the number of open connections by reusing existing ones
        opts.connectionDropTime = 5
        opts.connectionTimeout = 5
        opts.noDelay = true

        let params = NWParameters(tls:nil, tcp: opts)
        params.allowLocalEndpointReuse = true // that's not really useful, but since I've seen it in many places, I've decided to leave it for now

        print("TCP port \(p)")
        if let l = try? NWListener(using: params, on: NWEndpoint.Port(rawValue: p)!) {
            listener = l
            print("TCP state \(String(describing: l.state ))")
            self.port = NWEndpoint.Port(rawValue: p)!
        }
    }  

func start() {
    curStartAttempt = 0
    doStart()
}

func doStart() {
        guard let l = listener else {
            toast("Couldn't start listener", "Try rebooting your phone and the app", ERROR_DUR) // some custom toast to show to a user
            print ("Couldn't start listener: \(self.port.rawValue)")
            return
        }
        print("TCP start \(String(describing: l.state ))")

        l.stateUpdateHandler = self.stateDidChange(to:)
        l.newConnectionHandler = self.didAccept(nwConnection:)
        
        l.start(queue: .main)
    }

   // Below is the most important function that handles
   // "address in use" error gracefully

   func stateDidChange(to newState: NWListener.State) {
        switch newState {
        case .ready:
            print("Server ready \(self.port.rawValue)")
        case .failed(let error):
            print("Server failure, error: \(error.localizedDescription)")
            if (curStartAttempt < maxStartAttempts) {
                curStartAttempt += 1
                listener?.cancel()
                let deadlineTime = DispatchTime.now() + .milliseconds(1000)
                DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
                    self.initPort(self.port.rawValue)
                    self.doStart()
                }
            }
            else {
                loading = nil
                toast("Listener Error", "Try rebooting your phone and the app", ERROR_DUR) // just in case it fails, but it has never happened so far in my case
            }
        default:
            break
        }
    }
} // End of TcpListener class

这比我之前的示例更简单,最重要的是,它总是有效。

为了解决用户体验问题,您可能想告诉他们在启动新侦听器时发生了一些事情。这就是我为解决这个问题所做的:

// Function returning a customized progress view    

func progressView(_ text: String?) -> AnyView {
        let label = Label(text ?? "", systemImage: "network")
                        .font(Font(UIFont.preferredFont(forTextStyle:.caption1)))
                        .foregroundColor(Color.orange)
        
        return AnyView(ProgressView{
            return label
        }
        .frame(maxWidth: .infinity, alignment:.center)
        )
    }

// This is how the function is used in another view
// "loading" is a @State variable containing text to display
// or nil when you don't want to show the progress view
    
func listView () -> AnyView {
    
        AnyView (
        List() { // This is just my custom list view class
    
            if (loading != nil) {
    
                progressView(loading)
                    .frame(maxWidth: .infinity, alignment:.topLeading)
    
            }
            else {
                   AnyView(EmptyView())
            }

...
} // End of listView function

以下是 iPhone 中进度视图的外观

【讨论】:

    【解决方案2】:

    这是我为 iOS 使用 Bonjour 按照 Apple 的 TicTokToe 示例应用程序执行此操作的方式。如果您不使用 iOS 和 Bonjour,这可能会或不会对您有用,但 Apple 示例专门使用 iOSBonjour

    我创建了一个名为 PeerListener 的 NWListener 类:

    protocol PeerListenerDelegate: class {
        func create(_ connection: NWConnection)
    }
    
    class PeerListener {
        
        weak var delegate: PeerListenerDelegate?
        
        private var listener: NWListener?
        
        private var params: NWParameters?
        
        init (delegate: PeerListenerDelegate) {
            self.delegate = delegate
            
            let tcpOptions = NWProtocolTCP.Options()
            // ...
            let parameters = NWParameters(tls: nil, tcp: tcpOptions)
            // ...
            
            self.params = parameters
            
            initListener()
        }
        
        private func initListener() {
            
            if self.delegate == nil { return }
            
            guard let params = self.params else { return }
            
            do {
                
                listener = try NWListener(using: params)
                
                listener?.service = NWListener.Service(name: "MyName", type: "_myApp._tcp")
                
                startListening()
                
            } catch let err as NSError {
                print("Failed to create listener", err.debugDescription)
            }
        }
    }
    
    // MARK: - StateUpdateHandler
    extension PeerListener {
    
        private func startListening() {
            
            guard let listener = listener else { return }
            
            listener.stateUpdateHandler = { [weak self](newState) in
                
                switch newState {
                // ...
                case .failed(let error):
                    
                    print("Listener failed with \(error), restarting")
                    self?.cancelAndRestartListener()
                    
                default:break
                }
            }
            
            receivedNewConnectionFrom(listener)
            
            listener.start(queue: .main)
        }
        
        private func receivedNewConnectionFrom(_ listener: NWListener) {
            
            listener.newConnectionHandler = { [weak self](nwConnection) in
                
                self?.delegate?.create(nwConnection)
            }
        }
    }
    
    // MARK: - Supporting Functions
    extension PeerListener {
        
        private func cancelAndRestartListener() {
            
            listener = nil
            initListener()
        }
        
        public func setListenerToNil() {
            
            listener?.cancel()
            listener = nil
        }
    }
    

    在我设置监听器的vc里面:

    ViewController: UIViewController {
    
        var listener: PeerListener?
        
        var connections: [PeerConnectionIncoming]() // https://stackoverflow.com/a/60330260/4833705
    
        override func viewDidLoad() {
            super.viewDidLoad()
        
            startListener()
        }
        
        func startListener() {
        
            listener = PeerListener(delegate: self)
        }
    
        func stopListener() {
    
            listener?.setListenerToNil()
            listener = nil
        }
    
        @objc func didEnterBackground() { // Background Notification Selector Method
            
            stopListener()
        }
        
        @objc func appWillEnterForeground() { // Foreground Notification Selector Method
            
            if listener == nil {
    
                startListener()
            }
        }
    }
    
    // MARK: - class must conform to the PeerListenerDelegate method
    extension ViewController: PeerListenerDelegate {
        
        func create(_ connection: NWConnection) {
    
            // create a NWConnection here and keep a reference to it inside an array. You can use this answer for guidance: https://stackoverflow.com/a/60330260/4833705
    
            let peerConnectionIncoming = PeerConnectionIncoming(connection: connection, delegate: self)
    
            connections.append(peerConnectionIncoming) // *** IT IS IMPORTANT THAT YOU REMOVE THE CONNECTION FROM THE ARRAY WHEN THE CONNECTION IS CANCELLED ***
        }
    }
    
    // MARK: - class must conform to the PeerConnectionIncomingDelegate method
    extension ViewController: PeerConnectionIncomingDelegate {
    
        func receivedIncoming(_ connection: NWConnection) {
    
            connection.receive(minimumIncompleteLength: 1, maximumLength: 65535) { [weak self](data, context, isComplete, error) in
            print("\nConnection Received: \(data?.count as Any) data bytes, from endPoint: \(connection.endpoint)")
            
                if let err = error {
                    print("Recieve Error: \(err.localizedDescription)")
                    return
                }
            
                if let data = data, !data.isEmpty {
                
                    // do something with data
    
                } else {
                    print("-=-=-=-=-= Receive data is nil -=-=-=-=-=")
                }
            }
        }
    }
    

    NWConnection 类名为PeerConnectionIncoming:

    protocol PeerConnectionIncomingDelegate: class {
        
        func receivedIncoming(_ connection: NWConnection)
    }
    
    class PeerConnectionIncoming {
    
        weak var delegate: PeerConnectionIncomingDelegate?
    
        private var connection: NWConnection?
        
        init(connection: NWConnection, delegate: PeerConnectionIncomingDelegate) {
            self.delegate = delegate
            self.connection = connection
            
            startConnection()
        }
    
        func startConnection() {
            
            guard let connection = connection else { return }
            
            connection.stateUpdateHandler = { [weak self](nwConnectionState) in
                
                switch nwConnectionState {
                case .preparing: print("\n..... Connection Incoming -Preparing .....\n")
                case .setup: print("Connection Incoming -Setup")
                case .waiting(let error): print("Connection Incoming -Waiting: ", error.localizedDescription)
                    
                case .ready:
                    
                    print("\n>>>>> Connection Incoming -Ready <<<<<\n")
                    
                    self?.delegate?.receivedIncoming(connection)
                    
                case .cancelled:
                    
                    // *** you need to remove the connection from the array in the ViewController class. I do this via AppDelegate ***
                    
                case .failed(let error):
                    
                    // *** you need to remove the connection from the array in the ViewController class. I do this via AppDelegate ***
                    
                default:break
                }
            }
            
            connection.start(queue: .main)
        }
    }
    

    【讨论】:

    • 您是否尝试将应用程序发送到后台,然后快速返回前台?如果你这样做了,你有没有看到任何应用程序崩溃?您所做的似乎没有什么不同,但在我的情况下,它导致了 OP 在 iOS 14.x 中报告的应用程序崩溃
    • 完全没有崩溃。您是否按照我的方式尝试了代码?
    • 我在 2 天的时间里一直在使用这种方法玩我的应用程序。我出于完全不同的原因进行测试,但应用程序从未崩溃。我在真实设备和模拟器上进入后台和前台至少 100 倍,而不是 1 次崩溃。最初,当我按照您在问题中所做的那样做时,它崩溃了,但这阻止了它。我只是连续快速来回 10 次,没有崩溃。
    • 这就是我的崩溃的地方:listener.start(queue: .main)。奇怪的是你不能使用 try/catch。你在哪个版本的操作系统上?是iOS吗?我需要重写所有内容才能按照你的方式完成
    • 我真的非常感谢你发布了一个使用 Bojour 协议的例子。我仍然希望看到“创建”功能。同时,我知道由于客户的原因,您的示例不适用于我的情况。不确定OP。可能它会为他工作。在任何情况下,我的 +1 都会提交给您,因为它告诉我,至少 Bonjour 不会因为这种方法而崩溃。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-11
    • 2013-12-24
    • 2014-12-13
    相关资源
    最近更新 更多