Swift NWListener 监听、取消、重新监听成功? [英] Swift NWListener listen, cancel, and relisten successfully?

查看:21
本文介绍了Swift NWListener 监听、取消、重新监听成功?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个应用程序,我应该有一个 websocket 只在应用程序处于前台时才侦听我已经进入生命周期通知并调用 start() 并相应地停止.应用程序正常运行,直到应用程序返回前台,此时我收到了许多警告和错误.

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

日志:

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?

推荐答案

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

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.

需要在您的自定义 TCP 侦听器类(以下示例中为 TcpListener)中完成更改.这就是 init 在我的情况下的样子:

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

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

Below is how the progress view looks in iPhone

这篇关于Swift NWListener 监听、取消、重新监听成功?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆