Should one use NWListener? POSIX Address already in use

Let's say you want to stop a server.

https://developer.apple.com/forums/thread/75997

From searching apparently, there's an automatic cooldown. Don't know whether it's true or not.

That thread mentions socket variables, that I don't believe can be used with the NW stuff.

NWListener "cancel" doesn't seem to stop a server? Eitherways, doing that and trying to use .start and something like

self.listener = try NWListener(using: self.cfg_nwParameters, on: self.port)

self.listener?.start(queue: .main)

this will trigger Address already in use if you "stopped" a server, because apparently you can't stop a server with NWListener. Because the socket isn't actually closing apparently.

Answered by DTS Engineer in 808111022

The Network framework equivalent of SO_REUSEADDR is the allowLocalEndpointReuse property. Are you setting that on the parameters you use to create the listener?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The Network framework equivalent of SO_REUSEADDR is the allowLocalEndpointReuse property. Are you setting that on the parameters you use to create the listener?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Please reply as a reply; if you reply in the comments, I may not see it. See Quinn’s Top Ten DevForums Tips for this and other titbits.

As to your actual issue, I’m not entirely sure what you’re getting at here. Are you still having issues? If so, can you walk me through a concrete example of the problem? That is, what steps do you take, when did it fail, and what were the symptoms of that failure?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Create a NWListener

Wait for a connection.

Connect the Client

Run some things

However, then trigger .cancel() while the client is still connected

Result is after trying to start again it claims that the POSIX is still being taken. EVEN after I close the app, which is strange. That means that I need to wait time for some reason.

OK, I need more details. Let me explain my best guess and then you can correct me if I’m wrong:

  1. On the server, you create a set of parameters using NWParameters.tcp.

  2. And set allowLocalEndpointReuse.

  3. You then create and start an NWListener for a specific port.

  4. On the client, you open a TCP connection to that server.

  5. On the server, the listener calls your new connection callback with an NWConnection.

  6. You exchange data on that connection.

Is that right so far?

At this point I’m not unsure as to the next step. You wrote:

However, then trigger .cancel() while the client is still connected

Are you calling cancel() on the connection? Or the listener? Or both?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

func cancelConnection(_ connection: NWConnection) {
    if let index = self.connectionsArray.firstIndex(where: { $0 === connection }) {
        self.connectionsArray.remove(at: index)
    }
    connection.cancel()
}

func stopServer() {
    for connection in self.connectionsArray {
        self.cancelConnection(connection) // This closes the connection
    }

    self.listener?.cancel()

    //self.listener?.stateUpdateHandler = nil
    //self.listener?.newConnectionHandler = nil
    //self.listener = nil
}

OK. Then let’s use that info to extend the steps in my previous post:

  1. On the server, you create a set of parameters using NWParameters.tcp.

  2. And set allowLocalEndpointReuse.

  3. You then create and start an NWListener for a specific port.

  4. On the client, you open a TCP connection to that server.

  5. On the server, the listener calls your new connection callback with an NWConnection.

  6. You exchange data on that connection.

  7. You stop the server by calling cancel() on all of its connections and then on the listener.

  8. You repeat steps 1 through 3.

What happens then?

When I tried this with the test code below, I see this sequence:

listener did change state, new: ready
listener will start connection
listener did start connection
connection did change state, new: preparing
connection did change state, new: ready
connection did receive, count: 6
listener will stop
listener did stop
connection did receive, EOF
listener did change state, new: failed(POSIXErrorCode(rawValue: 48): Address already in use)
connection did fail, error: POSIXErrorCode(rawValue: 89): Operation canceled

I believe I’m seeing this because, due to the async nature of Network framework, the connection hasn’t finished shutting down at the point that I try to restart my listener. Notably, if I uncomment the lines that delay the listener start by a second, the problem goes away.

Is that an accurate reflection of your issue?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import Network

class Server {
    var listenerQ: NWListener? = nil
    var connections: [NWConnection] = []

    func start() {
        let params = NWParameters.tcp
        params.allowLocalEndpointReuse = true
        let listener = try! NWListener(using: params, on: 12345)
        listener.stateUpdateHandler = { s in
            print("listener did change state, new: \(s)")
        }
        listener.newConnectionHandler = { connection in
            self.didAcceptConnection(connection)
        }
        listener.start(queue: .main)
        self.listenerQ = listener
    }
    
    func stop() {
        print("listener will stop")
        for connection in self.connections {
            connection.stateUpdateHandler = nil
            connection.cancel()
        }
        connections.removeAll()
        if let listener = self.listenerQ {
            listener.stateUpdateHandler = nil
            listener.newConnectionHandler = nil
            listener.cancel()
            self.listenerQ = nil
        }
        print("listener did stop")
    }
    
    func didAcceptConnection(_ connection: NWConnection) {
        print("listener will start connection")
        connection.stateUpdateHandler = { s in
            print("connection did change state, new: \(s)")
        }
        self.startReceive(on: connection)
        connection.start(queue: .main)
        self.connections.append(connection)
        print("listener did start connection")
    }
    
    func startReceive(on connection: NWConnection) {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 2048) { content, _, isComplete, error in
            if let content {
                print("connection did receive, count: \(content.count)")
            }
            if isComplete {
                print("connection did receive, EOF")
            }
            if let error {
                print("connection did fail, error: \(error)")
                return
            }
            self.startReceive(on: connection)
        }
    }
}

func main() {
    let server = Server()
    server.start()
    DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
        server.stop()
//        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            server.start()
//        }
    }
    withExtendedLifetime(server) {
        dispatchMain()
    }
}

main()

I believe so? My issue doesn't happen if I change the port from TCP to UDP.

It seems to happen whenever I call NWConnection.receive, to receive from a target connection.

I also use the stateUpdateHandler as the callback instead to check if .ready to then process the connection. Maybe I should try the other way.

Also is there a specific reason for let listener = self.listenerQ or is it just for better optional type handling?

What I am actually doing, is that I don't even re-call .start but instead I re-use the constructor

the server does trigger .cancel() so I am expecting to be able to re-construct it at any times I think?

Regarding your main issue, I’m not 100% of the best way to address that. I’m still researching this but, for the moment, inserting an artificial delay should get things working.

I also use the stateUpdateHandler as the callback instead to check if .ready to then process the connection. Maybe I should try the other way.

You can if you want to, but it’s unlikely to affect this specific situation. In general, it’s fine to start I/O on a connection before the connection is ready. The I/O will proceed as soon as the connection becomes ready.

Also is there a specific reason for let listener = self.listenerQ or is it just for better optional type handling?

It’s all about the optional handling. As a matter of style, I try to unwrap an optional once up front and then work with the non-optional value.

What I am actually doing, is that I don't even re-call .start but instead I re-use the constructor

Right. In fact, you can’t call start(…) again. A specific Network framework object can only be started once.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Is this wrapable thing an issue?

var listener: NWListener? = nil

... rest of code

then self.listener?.start(queue: .main)

I use the approach that I do because I try to minimise the number of optionals in my code. Consider this:

self.listener?.start(queue: .main)

If self.listener happens to be nil, you can’t easily tell whether start(…) gets called. OTOH, in my code listener is non-optional, so the possibility of nil is excluded by the type system.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Regarding your main issue, I’m not 100% of the best way to address that. I’m still researching this

I think the best path forward here is to wait for the .cancelled event to come through to the listener before starting your new listener. Here’s how I tweaked my test code to do that:

func stop(stopCompleted: @escaping () -> Void) {
    print("listener will stop")
    for connection in self.connections {
        connection.stateUpdateHandler = nil
        connection.cancel()
    }
    connections.removeAll()
    if let listener = self.listenerQ {
        listener.stateUpdateHandler = { s in
            if case .cancelled = s {
                stopCompleted()
            }
        }
        listener.newConnectionHandler = nil
        listener.cancel()
        self.listenerQ = nil
    } else {
        stopCompleted()
    }
    print("listener did stop")
}

I then call it like so:

server.stop() {
    server.start()
}

Keep in mind that my test code is just that. In a real app the existing state update handler would have more logic in it, so you’d have to decide whether to follow this path or combine this new logic with your existing logic.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

That's an interesting thought. But that didn't work.

What I can say though, is that I can start and stop the server how many times I want, it will work fine.

But if I connect as a client to the server and then stop it, I will run into this POSIX Issue. This doesn't happen if I use UDP, btw. If I use the UDP Protocol, I don't recall running into this issue. Only TCP.

Since there's a character limit on the forum:

https://github.com/karl-police/ios_app_mic_pc_test/blob/main/iOS%20Mic%20Test/Include/TCPServer.swift

https://github.com/karl-police/ios_app_mic_pc_test/blob/e9c633f4d9dcd7a402a07922800ed7ead743fa7c/iOS%20Mic%20Test/ViewController.swift#L396

I think you might have to wait for each of the TCP connections to hit the .cancelled state as well. Orchestrating that should be ‘fun’.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I actually don't run this issue if I make my Client abort the connection first.

To simulate waiting I tried to wait before cancelling the listener to disconnect it, but that didn't work either.

I have the feeling that .receive and .send cause the issue, maybe they didn't finish? I am not sure.

I am also not sure if I use installTap correctly to send PCM

I used NWConnection.forceCancel and that solved the issue

Should one use NWListener? POSIX Address already in use
 
 
Q