Swift NWListener 监听、取消、重新监听成功? [英] Swift NWListener listen, cancel, and relisten successfully?
问题描述
我有一个应用程序,我应该有一个 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屋!