NetworkFramework how to attempt to connect to a local address when there's not WAN connection?

I'm using tcp sockets that by default will try to connect through a hostname, but if the local network does not have WAN access and cannot resolve the hostname it should attempt to connect through a local ip instead. How do I go about doing this? I've noticed that the connection will get stuck on preparing and will not timeout in a timely manner.


alternativeEndpoint is suposed to be this fallback local ip while the normal endpoint is a public hostname, the debugger is just logging strings and keeping track of connection states, networkStatusChanged will block the ui with a spinner whenever online is false


import UIKit
import Network
import Foundation

@objc protocol NetworkDelegate: class {
    func passData(data: String) // sends received data to the delegate
    func networkStatusChanged(online: Bool, connectivityStatus: String)
    @objc optional func viabilityChanged(isViable: Bool)
}

struct Connection {
    let connection: NWConnection
    let id: Int
}

class TCPConnection {
    var endpoint: NWEndpoint
    var alternativeEndpoint: NWEndpoint?
    let parameters = NWParameters.tcp
    var connection: NWConnection!
    var connections: [Connection] = []
    let myQueue = DispatchQueue(label: "Network Queue")
    weak var delegate: NetworkDelegate?
    weak var debuggerDelegate: NetworkDebugging?
    var online = false
    private static var nextID: Int = 0
    var activeConnectionID = 0
    private var bestConnectionID: Int = 0
    private var retryTimer: Timer?
    private var retryAttempts = 0
    private var maxRetryAttempts = 3
    
    init(endpoint: NWEndpoint, alternativeEndpoint: NWEndpoint) {
        self.endpoint = endpoint
        self.alternativeEndpoint = alternativeEndpoint
        commonInit()
    }
    
    init(endpoint: NWEndpoint) {
        self.endpoint = endpoint
        commonInit()
    }
    
    func commonInit() {
        parameters.multipathServiceType = .disabled
        parameters.expiredDNSBehavior = .allow
        connection = create(endpoint)
        lookForPathUpdate(on: connection)
        lookForBetterPath(on: connection)
        lookForConnectivity(on: connection)
    }
    
    func start() {
        _ = create(endpoint)
    }
    
    func create(_ myEndpoint:NWEndpoint) -> NWConnection {
        let currentID = TCPConnection.nextID
        debugPrint("Creating connection to \(myEndpoint)")
        let newConnection = NWConnection(to: myEndpoint, using: parameters)
        connections.append(Connection(connection: newConnection, id: currentID))
        bestConnectionID = currentID
        TCPConnection.nextID += 1
        
        newConnection.stateUpdateHandler = {
            (newState) in switch (newState) {
            case .ready:
                self.chooseBestConnection()
                self.connectionStateChangedNotifier(connectionID: self.bestConnectionID, newStatusFlag: true, connectivityStatus: "ready")                self.firstConnection = false
            case .waiting(let error):
                self.connectionStateChangedNotifier(connectionID: self.bestConnectionID, newStatusFlag: false, connectivityStatus: "waiting")
                self.debug("Waiting error: \(error)")
                if error == .posix(POSIXErrorCode.ECONNREFUSED) {
                    self.retryTimer?.invalidate()
                    self.retryTimer = Timer.scheduledTimer(withTimeInterval: 6.0, repeats: false) { timer in
                        newConnection.restart()
                    }
                    self.debug("Trying to connect again after being refused")
                } else if error == .dns(-65554) {
                    self.debug("Could not resolve hostname")
                    if let alternative = self.alternativeEndpoint {
                        self.connection = self.create(alternative)
                    }
                }
            case .failed(let error):
                self.connectionStateChangedNotifier(connectionID: self.bestConnectionID, newStatusFlag: false, connectivityStatus: "failed")
                self.debug("Failed: \(error)")
                newConnection.cancel()
                self.start()
            case .cancelled:
                self.connectionStateChangedNotifier(connectionID: self.bestConnectionID, newStatusFlag: false, connectivityStatus: "cancelled")
            case .preparing:
                self.connectionStateChangedNotifier(connectionID: self.bestConnectionID, newStatusFlag: false, connectivityStatus: "preparing")
            default:
                break
            }
        }
        
        self.receive(on: newConnection)
        debug("Starting Network Dispatch Queue")
        newConnection.start(queue: self.myQueue)
        return newConnection
    }
    
    func chooseBestConnection() {
        if let bestCon = connections.filter({$0.connection.state == .ready && $0.id == bestConnectionID}).first {
            connection = bestCon.connection
            activeConnectionID = bestCon.id
            lookForPathUpdate(on: connection)
            lookForBetterPath(on: connection)
            lookForConnectivity(on: connection)
        }
        for (index, con) in connections.enumerated().reversed() {
            if con.id != activeConnectionID && con.id != bestConnectionID {
                close(con: con.connection)
                connections.remove(at: index)
            }
        }
    }
    
    func lookForConnectivity(on con:NWConnection) {
        con.viabilityUpdateHandler = { (isViable) in
            self.delegate?.viabilityChanged?(isViable: isViable)
            if (!isViable) {
                self.debug("Connectivity down!!!")
                self.delegate?.networkStatusChanged(online: false, connectivityStatus: "connectivity down")
                self.online = false
            } else {
                debugPrint("NETWORK - Connectivity up!!!")
                if con.state == .ready {
                    self.delegate?.networkStatusChanged(online: true, connectivityStatus: "connectivity up")
                    self.online = true
                }
            }
        }
    }
    
    func lookForBetterPath(on con:NWConnection) {
        con.betterPathUpdateHandler = { (betterPathAvailable) in
            if (betterPathAvailable) {
                self.debug("Better path available")
                self.start()
            }
        }
    }
    
    func lookForPathUpdate(on con:NWConnection) {
        con.pathUpdateHandler = { path in
            if path.status == .satisfied {
                self.debug("Path is satisfied")
            } else {
                self.debug("Path is not satisfied")
            }
            if path.usesInterfaceType(.wifi) {
                self.debuggerDelegate?.changedToPath("Wifi")
            } else if path.usesInterfaceType(.cellular) {
                self.debuggerDelegate?.changedToPath("Cellular")
            }
        }
    }
    
    func isReady() -> Bool {
        if(connection.state == .ready) {
            return true
        }
        return false
    }
    
    func isOnline() -> Bool {
        return online
    }
    
    func isClosed() -> Bool {
        if(connection.state == .cancelled) {
            return true
        }
        return false
    }
    
    func sendMsg(_ message: String, completion: @escaping (NetworkError?) -> Void = {_ in }) {
        let msg = message + "\r\n"
        let data: Data? = msg.data(using: .utf8)
        debug("Sending: \(msg)")
        connection.send(content: data, completion: .contentProcessed { (sendError) in
            if let sendError = sendError {
                self.debug("\(sendError)")
                self.debug("Failed to send: \(message)")
                completion(.failedToSend)
                return
            }
            completion(nil)
            })
    }
    
    func sendPing(_ message: String)  {
        let msg = message + "\r\n"
        let data: Data? = msg.data(using: .utf8)
        debug("Sending: \(msg)")
        connection.send(content: data, completion: .idempotent)
    }
    
    func receive(on con: NWConnection) {
        con.receive(minimumIncompleteLength: 1, maximumLength: 8192) { (content, context, isComplete, error) in
            if let content = content {
                let delimiter = "\r\n"
                let response = String(decoding: content, as: UTF8.self).components(separatedBy: delimiter)
                if response[0] == "PONG" {
                    self.debug("PONG received!")
                }
                DispatchQueue.main.async {
                    self.tellViewController(message: String(decoding: content, as: UTF8.self))
                }
                self.debug("RECEIVED: \(String(decoding: content, as: UTF8.self))")
            }
            if isComplete {
                con.restart()
                self.debug("Received EOF")
            } else if let error = error {
                self.debug("Error receiving data - \(error)")
            } else if con.state == .ready && isComplete == false {
                self.receive(on: con)
            }
        }
    }
    
    func close(con: NWConnection) {
        con.stateUpdateHandler = nil
        con.viabilityUpdateHandler = nil
        con.betterPathUpdateHandler = nil
        con.pathUpdateHandler = nil
        if con.state != .cancelled {
            con.cancel()
        }
    }
    
    func close() {
        close(con: connection)
    }
    
    private func connectionStateChangedNotifier(connectionID: Int, newStatusFlag: Bool, connectivityStatus: String) {
        if connectionID == activeConnectionID {
            notifyDelegateOnChange(newStatusFlag: newStatusFlag, connectivityStatus: connectivityStatus)
        }
    }
    
    private func notifyDelegateOnChange(newStatusFlag: Bool, connectivityStatus: String) {
        debug("newStatusFlag: \(newStatusFlag), connectionState: \(connectivityStatus)")
        self.delegate?.networkStatusChanged(online: newStatusFlag, connectivityStatus: connectivityStatus)
        if newStatusFlag != self.online {
            self.online = newStatusFlag
        }
    }
    
    private func tellViewController(message: String) {
        DispatchQueue.main.async {
            self.delegate?.passData(data: message)
        }
    }
    
    private func debug(_ msg:String) {
        debuggerDelegate?.log(msg)
    }
    
    deinit {
        debugPrint("NETWORK - connection deinit")
        if connection.state != .cancelled {
            connection.cancel()
        }
    }
}

Accepted Reply

and will not timeout in a timely manner.

The definition of “timely manner” is where this falls down. In my experience, setting a good timeout is virtually impossible.

If I were in your shoes I’d start an

NWConnection
to the DNS name and, if that doesn’t connect promptly — and you don’t need to rely on some system default here, just set a timer for whatever timeout you want — start a second
NWConnection
for the local IP address.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

and will not timeout in a timely manner.

The definition of “timely manner” is where this falls down. In my experience, setting a good timeout is virtually impossible.

If I were in your shoes I’d start an

NWConnection
to the DNS name and, if that doesn’t connect promptly — and you don’t need to rely on some system default here, just set a timer for whatever timeout you want — start a second
NWConnection
for the local IP address.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Without closing the old one like this?

func startTimeoutClock(on con: NWConnection) {
        DispatchQueue.main.async {
            self.retryTimer?.invalidate()
            self.retryTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in
                if con.state == .preparing, con.currentPath?.usesInterfaceType(.wifi) != nil, let alternative = self.alternativeEndpoint {
                    self.debug("Connection timeout on \(con.endpoint), trying alternative endpoint \(alternative)")
                    _ = self.create(alternative)
                }
            }
        }
    }
  }

When it comes to bestPathUpdateHandler am I handling it correctly? Couldn't find any examples on how to use it

Without closing the old one like this?

If you close the old one, it’ll cancel that connection. Whether you want to do that or not is kinda up to you.

As to the code you posted, what I’d do in your situation is to have two levels of abstraction:

  • Provisional connections

  • The active connection

Your

TCPConnection
object can have an arbitrary number of the former but only one of the latter. A connection transitions from the former to the latter when it goes into the
.ready
state. At that point you set up your I/O state on that connection.

Right now you’re not drawing that distinction so, for example, your

create(…)
method calls
receive(on:)
with the connection it just created, meaning that you might then end up with multiple receives completing on two different connections.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"