Swift NWListener listen, cancel, and relisten successfully?

此生再无相见时 提交于 2021-01-02 07:07:49

问题


I have an app where I an supposed to have a websocket that listens only when the app is in the foreground I have tapped into the lifecycle notifications and call start() and stop accordingly. The app works correctly until the appication comes back to the foreground, at which point I get a number of warnings and errors.

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()
    }
}

Log:

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.

Even beyond the messages and the stacktrace, the listener is not listening. What do I have to do to be able to cancel listen and re-listen on the same port?


回答1:


I've finally found a better solution that works for all cases, but I need to tell you upfront that there is a waiting time involved. That waiting time ranges from a few millis when there are no active connections to 30 seconds (26 max in my case to be exact). If that's too much, you can safely skip this post, otherwise - keep reading.

The change needs to be done in your custom TCP listener class (TcpListener in the example below). This is how init looks like in my case:

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("Watch 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

It's a way simpler than my previous example and most importantly, it always works.

To address user's experience issue you might want to tell them that something is going on while the new listener is being launched. This is what I did to address that:

// 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

Below is how the progress view looks in iPhone




回答2:


This is the way I did it for iOS using Bonjour following Apple's TicTokToe example app. If you aren't using iOS and Bonjour maybe this will or won't work for you but the Apple example specifically uses iOS and Bonjour.

I created a NWListener class named PeerListener:

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
    }
}

Inside the vc where I set the Listener:

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 class named 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)
    }
}


来源:https://stackoverflow.com/questions/63178889/swift-nwlistener-listen-cancel-and-relisten-successfully

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!