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?
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)
}
}
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("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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With