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()
}
}
}