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