How to use NWConnection in place of or in addition to Bonjour?

I'm a complete noob to networking but I need local discovery to share ARKit Worldmaps. The MCSession peer limit of 8 users isn't enough for me because I need unlimited peer connections (all peer to peer). I decided to use Bonjour and got a discovery system setup. Where I got stumped is actually connecting user to user and sending data back and forth. To do that I need to create sockets for inputstreams and outputstreams and somehow add to the them to the NetService Delegate method:


func netService(_ sender: NetService, didAcceptConnectionWith inputStream: InputStream, outputStream stream: OutputStream) { }


All I know is I have to use CocoaAsyncSocket to make life easier but I'm still lost on what to do in the didAcceptConnectionWith and this is turning into a daunting task. In my reearch I came across the NWConnection class. From my understanding it's supposed to replace Bonjour but I don't have much reference on how to use it. For eg I followed this tutorial, this tutorial, and ths code from Eskimo but that's all that I could find.


There are 2 problems with NWConnection the first being unlike Bonjour which has delegate methods to find all local peers and to remove them:


// find
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { }

// remove 
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) { }


I cannot see how to do this with NWConnection. I did find this question where the op was trying to use the Bonjour service to discover address and then hook those into NWConnection as an endpoint but eskimo said not to do it that way. It does seem like a good idea to get around this issue I'm having.


The second problem is that I need the users of my app to discover all the other users around them. With the above Bonjour delgate method that is taken care. But with NWConnection it seems you have to set the endpoint in advance:


let connection = NWConnection(host: "example.com", port: 80, using: .tcp)
connection.stateUpdateHandler = self.stateDidChange(to:)


That's fine if you're connecting to a webpage but I don't see how one http endpoint correlates to local discovery for the users using my app.


Here is my Bonjour code below and how i use it in my ARKit VC


ARKitController:

import Network

class ARKitController: UIViewController {

    lazy var sceneView: ARSCNView = {
        let sceneView = ARSCNView()
        sceneView.translatesAutoresizingMaskIntoConstraints = false
        sceneView.delegate = self
        return sceneView
    }()

    let configuration = ARWorldTrackingConfiguration()

    var connection: NWConnection?
    var netBrowser: NetBrowser!
    var arrOfIPAddrresses = [String]()

    func overrideViewWillAppear(_ animated: Bool) {
       super.viewWillAppear(animated)

       netBrowser = NetBrowser()
       netBrowser.delegate = self
       netBrowser.startSearch()

       sceneView.session.run(configuration, options: [])
    }

    func overrideViewWillDisappear(_ animated: Bool) {
       super.viewWillDisappear(animated)

       netBrowser.stopDiscovery()
       netBrowser = nil
       sceneView.session.pause()
    }

    func saveIP(address: String) {

        arrOfIPAddresses.append(address)
    }

    func remove(_ service: NetService) {

        // get the ipaddress from the service then remove it from the array
    }

    func sendDataUsing(_ sockAddr: UnsafeBufferPointer ) {
        
        // get host and port from socket address ...
        
        let hostUDP = NEWEndpoint.Host ...
        let portUDP = NWEndpoint.Post ...

        connectToUDP(hostUDP, portUDP)
    }
}

//MARK:- NWConnection Methods
extension ARKitController {
    
    func connectToUDP(_ hostUDP: NWEndpoint.Host, _ portUDP: NWEndpoint.Port) {
        
        self.connection = NWConnection(host: hostUDP, port: portUDP, using: .udp)

        self.connection?.stateUpdateHandler = { (newState) in
            switch (newState) {
            case .ready:
                print("State: Ready\n")
                self.sendThisUsersWorldMap()
                self.receiveOtherUsersWorldMap()
            case .setup:
                print("State: Setup\n")
            case .cancelled:
                print("State: Cancelled\n")
            case .preparing:
                print("State: Preparing\n")
            default:
                print("ERROR! State not defined!\n")
            }
        }
        
        self.connection?.start(queue: .global())
    }
    
    func sendThisUsersWorldMap() {

        sceneView.session.getCurrentWorldMap { (worldMap, error) in
            guard let map = worldMap else { print("Error: \(error!.localizedDescription)") return }
            guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true) 
                  else { print("can't encode map") return }
            
            self.connection?.send(content: data, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
                if (NWError == nil) {
                    print("Data was sent to UDP")
                } else {
                    print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
                }
            })))
        }

    }
    
    func receiveOtherUsersWorldMap() {
        self.connection?.receiveMessage { (data, context, isComplete, error) in
            if (isComplete) {
             print("Receive is complete")
                if let data = data {
                    
                    self.mergeOtherUsersWorldMap(data)
                }
            }
        }
    }

}


Bonjour:

protocol NetBrowserDelegate: class {   
    func saveIP(address: String)
    func remove(_ service: NetService)
    func sendDataUsing(_ sockAddr: UnsafeBufferPointer)
}

class NetBrowser: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
   
    var browser: NetServiceBrowser?
    var services = [NetService]()
    let domain = "local."
    let name = "_http._tcp"
   
    weak var delegate: NetBrowserDelegate?

    func startSearch() {

        services.removeAll()
        browser = NetServiceBrowser()
        browser?.includesPeerToPeer = true
        browser?.delegate = self

        browser?.stop()
       
        browser?.schedule(in: RunLoop.current, forMode: .default)
        browser?.searchForServices(ofType: self.name, inDomain: self.domain)
        RunLoop.current.run()
    }
   
    func stopDiscovery() {
        browser?.stop()
        browser?.delegate = nil
        browser = nil
    }
}

// MARK:- Delegate Methods
extension NetBrowser {
   
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        print("found service")
        services.append(service)
       
        service.delegate = self
        service.publish(options: NetService.Options.listenForConnections)
        service.resolve(withTimeout: 5.0)
    }
   
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
        if let index = self.services.firstIndex(of:service) {
            self.services.remove(at:index)
            print("removing a service")
           
            delegate?.remove(service)
        }
    }
   
    func netServiceDidResolveAddress(_ sender: NetService) {
        print("netServiceDidResolveAddress get called with \(sender).")
       
        var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
       
        guard let data = sender.addresses?.first else {
            print("guard let data failed")
            return
        }
       
        data.withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> Void in
            let sockaddrPtr = pointer.bindMemory(to: sockaddr.self)
            guard let unsafePtr = sockaddrPtr.baseAddress else { return }
            guard getnameinfo(unsafePtr, socklen_t(data.count), &hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST) == 0 else {
                return
            }

            delegate?.sendDataUsing(sockaddrPtr)
        }
        let ipAddress = String(cString:hostname)
        print(ipAddress)

        delegate?.saveIP(address: ipAddress)
    }
   
    func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
        print("netServiceDidNotResolve:\(sender)");
    }
}

extension NetBrowser {
   
    func netService(_ sender: NetService,
                    didAcceptConnectionWith inputStream: InputStream,
                    outputStream stream: OutputStream) {
        print("netServiceDidAcceptConnection:\(sender)");
    }
   
    func netServiceWillPublish(_ sender: NetService) {
        print("netServiceWillPublish:\(sender)");  //This method is called
    }
   
    func netServiceDidPublish(_ sender: NetService) {
        print("netServiceDidPublish:\(sender)");
    }
   
    func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) {
        //debugPrint(errorDict)
        print("didNotPublish:\(sender)")
    }
}

Accepted Reply

NWConnection would be used in addition to Bonjour for a case like this. If using Network framework a usual workflow would include NWListener, NWBrowser, and one or more NWConnections. Where the listener is broadcasting a Bonjour service on a domain over a local network and the clients are browsing for this service to open up a connection to the service endpoint the listener is providing. NWConnection would be used to connect from the client to the device listener and then provide a transport mechanism to send data back and forth.



As to your question, if all of your clients truly need to be peer-to-peer with each other then Multipeer Connectivity framework is what you are looking for. If the devices you want to send data to are all connecting to a listener that is setup on a single device, then Network Framework is what you are looking for.


As an brief example, your NWListener you be setup to broadcast a service:

/// NWListener

let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true

let listener = try NWListener(using: params)

listener.service = NWListener.Service(name: "YourService",
                                      type: "_service._udp")

listener.stateUpdateHandler = { newState in
    switch newState {
    case .ready:
        if let port = listener.port {
            // Listener setup on a port.  Active browsing for this service.
        }
    case .failed(let error):
        
        listener.cancel()
        os_log("Listener - failed with %{public}@, restarting", error.localizedDescription)
        // Handle restarting listener
    default:
        break
    }
}

// Used for receiving a new connection for a service.
// This is how the connection gets created and ultimately receives data from the browsing device.
listener.newConnectionHandler = { newConnection in
    // Send newConnection (NWConnection) back on a delegate to set it up for sending/receiving data
}

// Start listening, and request updates on the main queue.
listener.start(queue: .main)


Next, setup a browser on a client to browse for the Bonjour service the listener is broadcasting.

/// NWBrowser

let params = NWParameters()
params.includePeerToPeer = true

let browser = NWBrowser(for: .bonjour(type: "_service._udp",
                                      domain: "local"), using: params)

browser.stateUpdateHandler = { newState in
    switch newState {
    case .failed(let error):
        browser.cancel()
        // Handle restarting browser
        os_log("Browser - failed with %{public}@, restarting", error.localizedDescription)
    case .ready:
        os_log("Browser - ready")
    case .setup:
        os_log("Browser - setup")
    default:
        break
    }
}

// Used to browse for discovered endpoints. 
browser.browseResultsChangedHandler = { results, changes in
    for result in results {
        os_log("Browser - found matching endpoint with %{public}@", result.endpoint.debugDescription)
        
        // Send endPoint back on a delegate to set up a NWConnection for sending/receiving data
        break
    }
}

// Start browsing and ask for updates on the main queue.
browser.start(queue: .main)



Take the incoming endpoint from browseResultsChangedHandler and create a NWConnection to connect to the service.

/// NWConnection


var p2p2Connection: NWConnection
...


let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true
p2p2Connection = NWConnection(to: nwEndpoint, using: params) // Discovered browsing endPoint

...


// Here is where you could potentially react to failures in connecting to the endpoint discovered by your browser
p2p2Connection.stateUpdateHandler = { newState in
            
  switch newState {
  case .ready:
     os_log("Connection established")
     
  case .preparing:
     os_log("Connection preparing")
  case .setup:
     os_log("Connection setup")
  case .waiting(let error):
     os_log("Connection waiting: %{public}@", error.localizedDescription)
  case .failed(let error):
     os_log("Connection failed: %{public}@", error.localizedDescription)
     
     // Cancel the connection upon a failure.
     self.p2p2Connection.cancel()
     // Notify your delegate that the connection failed with an error message.
  default:
     break
  }
}

// Start the connection and send receive responses on the main queue.
p2p2Connection.start(queue: .main)


// Setup the receive method to ensure data is captured on the incoming connection.
receiveIncomingDataOnConnection()



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Replies

NWConnection would be used in addition to Bonjour for a case like this. If using Network framework a usual workflow would include NWListener, NWBrowser, and one or more NWConnections. Where the listener is broadcasting a Bonjour service on a domain over a local network and the clients are browsing for this service to open up a connection to the service endpoint the listener is providing. NWConnection would be used to connect from the client to the device listener and then provide a transport mechanism to send data back and forth.



As to your question, if all of your clients truly need to be peer-to-peer with each other then Multipeer Connectivity framework is what you are looking for. If the devices you want to send data to are all connecting to a listener that is setup on a single device, then Network Framework is what you are looking for.


As an brief example, your NWListener you be setup to broadcast a service:

/// NWListener

let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true

let listener = try NWListener(using: params)

listener.service = NWListener.Service(name: "YourService",
                                      type: "_service._udp")

listener.stateUpdateHandler = { newState in
    switch newState {
    case .ready:
        if let port = listener.port {
            // Listener setup on a port.  Active browsing for this service.
        }
    case .failed(let error):
        
        listener.cancel()
        os_log("Listener - failed with %{public}@, restarting", error.localizedDescription)
        // Handle restarting listener
    default:
        break
    }
}

// Used for receiving a new connection for a service.
// This is how the connection gets created and ultimately receives data from the browsing device.
listener.newConnectionHandler = { newConnection in
    // Send newConnection (NWConnection) back on a delegate to set it up for sending/receiving data
}

// Start listening, and request updates on the main queue.
listener.start(queue: .main)


Next, setup a browser on a client to browse for the Bonjour service the listener is broadcasting.

/// NWBrowser

let params = NWParameters()
params.includePeerToPeer = true

let browser = NWBrowser(for: .bonjour(type: "_service._udp",
                                      domain: "local"), using: params)

browser.stateUpdateHandler = { newState in
    switch newState {
    case .failed(let error):
        browser.cancel()
        // Handle restarting browser
        os_log("Browser - failed with %{public}@, restarting", error.localizedDescription)
    case .ready:
        os_log("Browser - ready")
    case .setup:
        os_log("Browser - setup")
    default:
        break
    }
}

// Used to browse for discovered endpoints. 
browser.browseResultsChangedHandler = { results, changes in
    for result in results {
        os_log("Browser - found matching endpoint with %{public}@", result.endpoint.debugDescription)
        
        // Send endPoint back on a delegate to set up a NWConnection for sending/receiving data
        break
    }
}

// Start browsing and ask for updates on the main queue.
browser.start(queue: .main)



Take the incoming endpoint from browseResultsChangedHandler and create a NWConnection to connect to the service.

/// NWConnection


var p2p2Connection: NWConnection
...


let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true
p2p2Connection = NWConnection(to: nwEndpoint, using: params) // Discovered browsing endPoint

...


// Here is where you could potentially react to failures in connecting to the endpoint discovered by your browser
p2p2Connection.stateUpdateHandler = { newState in
            
  switch newState {
  case .ready:
     os_log("Connection established")
     
  case .preparing:
     os_log("Connection preparing")
  case .setup:
     os_log("Connection setup")
  case .waiting(let error):
     os_log("Connection waiting: %{public}@", error.localizedDescription)
  case .failed(let error):
     os_log("Connection failed: %{public}@", error.localizedDescription)
     
     // Cancel the connection upon a failure.
     self.p2p2Connection.cancel()
     // Notify your delegate that the connection failed with an error message.
  default:
     break
  }
}

// Start the connection and send receive responses on the main queue.
p2p2Connection.start(queue: .main)


// Setup the receive method to ensure data is captured on the incoming connection.
receiveIncomingDataOnConnection()



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hi meaton,


Thanks for the reply. Sorry for the late response, I've been working on some ARKit related stuff and it had me consumed since yesterday. I have to meticulously go through this answer and try to understand it as I'm new to networking. I have some questions but I want to get a better understanding of the flow before I ask anything (I might be able to figure them out on my own). I'll send you an updated reponse by Wed evening,


ttys and thanks agiain!

Lance

Hi meaton,


I've been going over this code for hours and searching around the internet for help. I'm starting to understand the flow, I should have something ready by later today. I wanted to ask a question about a point you brought up:


> if all of your clients truly need to be peer-to-peer with each other then Multipeer Connectivity framework is what you are looking for


with the code you added, why can't I have each client act as host and guest? Once they are discovered they share their data and once they discover someone else they pull that data. I don't know enough about networking to know if that creates other problems.


The Multipeer Connectivity api which MCSession and has a peer limit of 8 peers. Im creaitng a shared experience for ARKit. Initally I thought that the peer limit had something to do with ARKit but as I did more research I found out that even creating a messaging app MCSession will still limit you to 8 peers. That's where the probelm comes in with Multipeer Connectivity, it's easier to use but you have the hard limit. I haven't found anything on the internet that explains why it has that hard 8 peer limit even though it's built on top of Bonour which has no limit. It reminds me of the UIImagePicker or a UITableView which is easier to use but you lose the flexibility of the much more difficult AVFoundation or a UICollectionView.

Lance,


You could setup each app as a listener and a browser with Network Framework. There is just a lot more complexity to manage around who is broadcasting what service and how each client connects to the service. Once you get setup with 2 or 3 services, the question becomes, do all of my clients need to browse for each service and open a connection? This becomes complex fast. That is why I suggested the more traditional client/server approach with Network Framework.

| with the code you added, why can't I have each client act as host and guest? Once they

| are discovered they share their data and once they discover someone else they pull that

| data. I don't know enough about networking to know if that creates other problems.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

hi meaton,


I thought a lot about what you said about the compexitiy of everyone being advertiser & browser and I decided for now I'm going to just stick with Multipeer Connectivity. I have to do more more research into getting what I want accomplished.


I finally got a good grasp on your excellent code but I do have a couple of questions about it:


-at the bottom of the p2p2Connection class on line 45 you have receiveIncomingDataOnConnection(). Is this supposed to be a delegate method that I create?


// Setup the receive method to ensure data is captured on the incoming connection.
receiveIncomingDataOnConnection()


-in that same p2p2Connection class, in the p2p2Connection.stateUpdateHandler = { newState in } on lines 20 & 21 case .ready os_log("Connection established"), can't I use this .ready state to send and receive data? I found a couple of samples of people doing this on Stackoverflow.


// Here is where you could potentially react to failures in connecting to the endpoint discovered by your browser 
p2p2Connection.stateUpdateHandler = { (newState) in
    switch (newState) {
    case .ready:
    //The connection is established and ready to send and recieve data.
    self.sendPaket("Hello")
    self.receive()
    default:
    }
}

func sendPaket(_ packet:String) {
    let packetData = packet.data(using: .utf8)
    self.connection?.send(content: packetData, completion: NWConnection.SendCompletion.contentProcessed(({ (error) in
    if let err = error {
        print("Sending error \(err)")
    } else {
      print("Sent successfully")
    }
  })))
}

// once data is received use a delegate method to send it to the the vc that's using this class
func receive() {
   self.connection?.receiveMessage(completion: { (data, context, isComplete, error) in
      if let err = error {
          print("Recieve error: \(err)")
          return
      }
      if let rcvData = data,
          let str = String(data:rcvData, encoding: .utf8) {
       print("Received: \(str) now pass it over to the delegate method")
      }
   })
}

You are correct, I never showed an example of using the receiveIncomingDataOnConnection(). This method is not meant to be a delegate method, but rather a function to setup the receiveMessage() method on p2p2Connection to receive data. An implementation of that function would look something like:


func receiveIncomingDataOnConnection() {
    
    p2p2Connection.receiveMessage { (content, _, _, error) in
        if let messageData = content {
            // Handle the messageData here.
        }
        if error == nil {
            // Called to ensure future data can be read off the connection.
            self.receiveIncomingDataOnConnection()
        }
    }
}

| -at the bottom of the p2p2Connection class on line 45 you have

| receiveIncomingDataOnConnection(). Is this supposed to be a delegate method that I

| create?

|

| // Setup the receive method to ensure data is captured on the incoming connection.

| receiveIncomingDataOnConnection()


Essentially, yes. When the connection enters the ready state you can start sending data on the connection as it makes sense for your application. Another option is to notify a managing object that the connection is now in the ready state and the managing object can dictate the flow of data on the connection.


func sendDataOnConnection(data: Data) {
    
    // Create a frame or some formatted data to send on the connection.
            
    p2p2Connection.send(content: data, completion: NWConnection.SendCompletion.contentProcessed { error in
        if let error = error {
            os_log("Error sending data: %{public}@", error.localizedDescription)
        }
    })
}

| -in that same p2p2Connection class, in the p2p2Connection.stateUpdateHandler = {

| newState in } on lines 20 & 21 case .ready os_log("Connection established"), can't I

| use this .ready state to send and receive data? I found a couple of samples of people

| doing this on Stackoverflow.



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Thank you for the help! Without you I would be lost right now.


Much appreciated and take care 🙂

No problem at all. Glad to help.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com