NWConnection as UDP Client stops transmitting after losing and regaining viability

I'm at a bit of a dead-end here and hope someone can shed some light.


Recently refactored a macOS app that sends and receives data on a wired LAN to use Network Framework, and through some stumbling around I've gotten UDP receive (NWListener) working great.


Here's my issue: NWConnection acting as a UDP client (send-only) is working great seemingly -- but once the network connection is lost and returns (ie: unplug the Ethernet cable and plug it back in, or any other condition that would cause connectivity to be lost), the NWConnection simply stops sending messages altogether until I quit and restart my app.


As soon as connectivity is lost and my app tries to send a UDP message, I see these errors in the console:

  • [] nw_socket_service_writes_block_invoke [C1:1] sendmsg(fd 7, 28 bytes) [49: Can't assign requested address]
  • [] nw_endpoint_flow_prepare_output_frames Failing the write requests [49: Can't assign requested address]


And apparently it is not able to recover after that.


I've checked the documentation, Googled and tried everything I can think of trial-and-error wise and I'm coming up dry.


Through all of it, the state never leaves "ready," but the viability does come and go as the network link goes down and up again as you'd epect.


From what I've read, this sort of connectivity issue should remedy/resolve itself within Network Framework and I shouldn't have to manually deal with it. But if I do, how would I go about it?


I've checked all the connection parameters, context meta data, etc. Really not sure what I'm doing wrong.


Thanks!


public class UDPClient {
    
    public final let name: String
    
    internal final var gcdThread: DispatchQueue = DispatchQueue.main // placeholder; custom thread will be created at class init
    internal final func makegcdThread() {
        gcdThread = DispatchQueue(label: specifics.app.bundleID + ".udpclient." + name)
    }
    
    private var connection: NWConnection?
    private var parameters: NWParameters!
    private var context: NWConnection.ContentContext?
    
    public internal(set) var host: NWEndpoint.Host
    public internal(set) var port: NWEndpoint.Port
    
    public init(name: String, host: NWEndpoint.Host, port: NWEndpoint.Port = 0, parameters: NWParameters? = nil, context: NWConnection.ContentContext? = nil) {
        self.name = name
        
        // cache address & port
        
        self.host = host
        self.port = port
        self.context = context
        
        makegcdThread()
        
        // port params
        
        let params: NWParameters
        if parameters != nil {
            // user parameers used
            params = parameters!
        } else {
            // default parameters used
            params = NWParameters.udp
            params.allowLocalEndpointReuse = true
            params.includePeerToPeer = true
        }
        self.parameters = params
        
        // set up connection
        
        connect()
    }
    
    internal final func connect() {        
        // new connection
        connection = NWConnection(host: host, port: port, using: self.parameters)
        
        // update handler
        connection?.stateUpdateHandler = { [weak self] state in
            // state seems to never change and is always in "ready" state for a UDP client...?
            self?.stateDidChange(to: state)
        }
        connection?.viabilityUpdateHandler = { [weak self] isViable in
            self?.viabilityDidChange(to: isViable)
        }
        
        // start connection
        connection?.start(queue: gcdThread)
    }
    
}

Accepted Reply

Looks like I may have answered my own question.


I was instancing my UDPClient class like this:


let metadata = NWProtocolIP.Metadata()
metadata.serviceClass = .responsiveData
let context = NWConnection.ContentContext(identifier: "OSC", metadata: [metadata])

udpClient = UDPClient(name: "udpclient", host: host, port: port, context: context)


By process of trial-and-error, the resolution was to remove the first 3 lines and just let Network Framework use default packet context in my constructor:


udpClient = UDPClient(name: "udpclient", host: host, port: port)


In hindsight, I think grabbing metadata from NWProtocolIP and using it for UDP was causing Network Framework to trip over itself and it wasn't obvious I had made that mistake. If anything, it should have been NWProtocolUDP.Metadata() but UDP metadata has no properties so there's no point even using it.


The only reason I was defining custom context/metadata was in hopes of elevating packet priority in the network (which is what .responsiveData plays a role in), however that is not available on UDP.

Replies

Looks like I may have answered my own question.


I was instancing my UDPClient class like this:


let metadata = NWProtocolIP.Metadata()
metadata.serviceClass = .responsiveData
let context = NWConnection.ContentContext(identifier: "OSC", metadata: [metadata])

udpClient = UDPClient(name: "udpclient", host: host, port: port, context: context)


By process of trial-and-error, the resolution was to remove the first 3 lines and just let Network Framework use default packet context in my constructor:


udpClient = UDPClient(name: "udpclient", host: host, port: port)


In hindsight, I think grabbing metadata from NWProtocolIP and using it for UDP was causing Network Framework to trip over itself and it wasn't obvious I had made that mistake. If anything, it should have been NWProtocolUDP.Metadata() but UDP metadata has no properties so there's no point even using it.


The only reason I was defining custom context/metadata was in hopes of elevating packet priority in the network (which is what .responsiveData plays a role in), however that is not available on UDP.

orchetect,


Thank you for posting your resolution. Digging in a bit deeper, how was NWProtocolIP.Metadata() integrated with UDPClient? I did not see this code being used in the first code sample and wanted to ask for anyone else referencing this post.


Also, this is odd behavior, and you are correct, the stateUpdateHandler should provide insight into the state of the connection.

Is there anything more that you can share in self?.stateDidChange(to: state) to get a better sense here also?


Through all of it, the state never leaves "ready," but the viability does come and go as the network link goes down and up again as you'd

epect.


From what I've read, this sort of connectivity issue should remedy/resolve itself within Network Framework and I shouldn't have to manually

deal with it. But if I do, how would I go about it?

> how was NWProtocolIP.Metadata() integrated with UDPClient?


Sorry, I realized that wasn't fully explained. I have updated my answer above with more detail.


> the stateUpdateHandler should provide insight into the state of the connection. Is there anything more that you can share in self?.stateDidChange(to: state) to get a better sense here also?


No actually, it's simply a method that prints the result of the state to the console. It only gets called once, at the first instantiation of my UDPClient class and enters the 'ready' state. It never changes from 'ready' state but that's likely by design and isn't an issue. After solving my problem, the NWConnection has no trouble resuming data transmission when network connectivity is restored. My theory is that NWConnection when set to .udp parameters has no need/purpose to change state?

To get a better look at the state of the of the connection when started or cancelled, you can use an exhaustive set of conditions to react based upon the connections state. To receive updates on whether the connection is viable or able to send data, use the viabilityUpdateHandler as this will react to link interruptions.

No actually, it's simply a method that prints the result of the state to the console. It only gets called once, at the first instantiation of my UDPClient class and enters the 'ready' state. It never changes from 'ready' state but that's likely by design and isn't an issue.