NWBrowser + NWListener + NWConnection

I am seeking assistance with how to properly handle / save / reuse NWConnections when it comes to the NWBrowser vs NWListener.

Let me give some context surrounding why I am trying to do what I am.

I am building an iOS app that has peer to peer functionality. The design is for a user (for our example the user is Bob) to have N number of devices that have my app installed on it. All these devices are near each other or on the same wifi network. As such I want all the devices to be able to discover each other and automatically connect to each other. For example if Bob had three devices (A, B, C) then A discovers B and C and has a connection to each, B discovers B and C and has a connection to each and finally C discovers A and B and has a connection to each.

In the app there is a concept of a leader and a follower. A leader device issues commands to the follower devices. A follower device just waits for commands. For our example device A is the leader and devices B and C are followers. Any follower device can opt to become a leader. So if Bob taps the “become leader” button on device B - device B sends out a message to all the devices it’s connected to telling them it is becoming the new leader. Device B doesn’t need to do anything but device A needs to set itself as a follower. This detail is to show my need to have everyone connected to everyone.

Please note that I am using .includePeerToPeer = true in my NWParameters. I am using http/3 and QUIC. I am using P12 identity for TLS1.3. I am successfully able to verify certs in sec_protocal_options_set_verify_block. I am able to establish connections - both from the NWBrowser and from NWListener. My issue is that it’s flaky. I found that I have to put a 3 second delay prior to establishing a connection to a peer found by the NWBrowser. I also opted to not save the incoming connection from NWListener. I only save the connection I created from the peer I found in NWBrowser. For this example there is Device X and Device Y. Device X discovers device Y and connects to it and saves the connection. Device Y discovers device X and connects to it and saves the connection. When things work they work great - I am able to send messages back and forth. Device X uses the saved connection to send a message to device Y and device Y uses the saved connection to send a message to device X.

Now here come the questions.

Do I save the connection I create from the peer I discovered from the NWBrowser?

Do I save the connection I get from my NWListener via newConnectionHandler?

And when I save a connection (be it from NWBrowser or NWListener) am I able to reuse it to send data over (ie “i am the new leader command”)?

When my NWBrowser discovers a peer, should I be able to build a connection and connect to it immediately? I know if I save the connection I create from the peer I discover I am able to send messages with it. I know if I save the connection from NWListener - I am NOT able to send messages with it — but should I be able to?

I have a deterministic algorithm for who makes a connection to who. Each device has an ID - it is a UUID I generate when the app loads - I store it in UserDefaults and the next time I try and fetch it so I’m not generating new UUIDs all the time. I set this deviceID as the name of the NWListener.Service I create. As a result the peer a NWBrowser discovers has the deviceID set as its name. Due to this the NWBrowser is able to determine if it should try and connect to the peer or if it should not because the discovered peer is going to try and connect to it.

So the algorithm above would be great if I could save and use the connection from NWListener to send messages over.

Answered by DTS Engineer in 834389022

I cover much of this ground in Moving from Multipeer Connectivity to Network Framework. I’m gonna recommend that you read that and then come back here with your follow-up questions. That way I can help you out and getting a better handle on what that post is missing [1].

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] It’s hard to know what you don’t know, you know? (-:

Accepted Answer
Would you be able to provide a simple example on how to use NWConnectionGroup with QUIC … ?

Sure.

Let’s first look at setting up the tunnel. This is very similar to setting up an NWConnection. On the client, do this:

let descriptor = NWMultiplexGroup(to: endpoint)
let parameters = NWParameters.quic(alpn: ["my-alpn"])
let group = NWConnectionGroup(with: descriptor, using: parameters)

where endpoint is as it would be for NWConnection.

On the server, set up your NWListener as you would for connections. The critical change is that you must set newConnectionGroupHandler rather than newConnectionHandler.

With the above you’ll be able to open a connection group on the client and accept it on the server. The next step is to create connections runner over those groups. There are two parts to that:

  • Initiating a client-to-server stream

  • Initiating a server-to-client stream

I’ll explain the first, covering both the client and server side of things, then come back to the second.

To initiate a client-to-server stream on the client, create an NWConnection from the connection group. There are two ways to do that, but the easiest is this:

let group: NWConnectionGroup = …
let connection = NWConnection(from: group)

Note that I’m letting both the endpoint and options parameters default to nil.

To accept a client-to-server stream on the server, set the newConnectionHandler property on the group.

Setting up a server-to-client stream is the reverse: The client sets newConnectionHandler on its group and the server calls NWConnection.init(from:to:using:).


In terms of the topology, this is just like it would be with TCP, or QUIC without using connection groups. You need a QUIC tunnel between each device pair, and you get to decide how to set that up. And once it’s set up, either device can start a stream from that tunnel by creating a connection from the group.


should I even be using QUIC?

It’s really up to you. QUIC has a number of key advantages:

  • There’s just one tunnel.

  • Which supports datagrams.

  • And multiple streams.

  • While handling TLS.

  • And flow control, both within a stream and for the tunnel as a whole.

  • And avoiding head-of-line blocking.

OTOH, it has some drawbacks:

  • You have to use TLS-PKI rather than TLS-PSK.

  • It’s significantly more complex.

If you have a simple app with one command-and-control stream and one real-time stream, using TCP for the first and UDP for the second is fine. It does, however, kinda box you in. If at some point in the future you end up needing to transfer a large resource, you have to choose between two unpleasant options (chunking or a separate TCP connection), whereas with QUIC you just start a new stream.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for the reply. Now for my sanity check...

Client -> NWBrowser

Server -> NWListener

Connection -> NWConnection

In the NWBrowser I discover NWEndpoints

With a discovered endpoint I can create a connection group (ie if I discovered 5 endpoints, excluding my own, I would then create 5 connection groups)

From a single connection group I can create many connections to the endpoint that was used to create the connection group.

When I create the connection group in the client I need to set newConnectionHandler to handle new incoming connections.

In NWListener I listen for new connection groups. I do this by setting newConnectionGroupHandler. For each group on the server i setup newConnectionHandler to handle new connections.


When everything is said and done and properly setup:

  • a device will have a list of groups (a group for each device it has discovered)
  • a device can have a list of connections that came from a group

In my head I see:

Client

  • client discovers endpoints
  • client creates connection groups
  • client setup connection group to listen for new connections
  • client can create new connections from a connection group
  • client is saving the connection group and the many connections

Server

  • server listens for new connection groups
  • server handles new incoming connection groups
  • server setup connection group to listen for new connections
  • service is saving the connection group and the many connections

Questions:

  1. What am I gaining from using a connection group vs how I was doing things before with just NWConnections?
  2. Is the benefit of using a connection group is that both sides (the server and the client) have a connection group so either one could simply create a new connection(ie let connection = NWConnection(from: group)) from the group and send data?
  3. See below for design plan / question

Design Use comparison of peer IDs to determine who initiates connection to who. So if this device's ID (this device will be an iPhone) is greater than the device of the discovered peer ID (the discovered device will be an iPad) then the iPhone will:

  • create a connection group from iPad endpoint
  • create a connection from the group to connect to the discovered peer iPad
  • the discovered peer iPad will then receive a new group connection
  • the discovered peer iPad will setup the group connection
  • the discovered peer iPad will receive a new connection on the connection group

In the end the iPhone will have a connection group and a single connection and the iPad will have a connection group and a single connection. Either side (iPhone or iPad) can easily create a new connection from their group connection. If they did, in our example, both sides would still have a single connection group but two connections now.

Is my thinking correct?

Now for my sanity check...

That sounds right.

What am I gaining from using a connection group vs how I was doing things before with just NWConnections?

The ability to run multiple streams over that connection group. For example, imagine you have a primary stream you use for short command-and-control messages and then you want to send a file. With QUIC you can create a new connection for transferring that file. That avoids head-of-line blocking issues and manages the flow control.

You could even use a separate stream for every command-and-control request/response pair. That might seem like overkill, but it’s exactly how HTTP/3 uses QUIC. And that gets you out of the business of framing and unframing, because each message occupies the entire stream.

Is the benefit of using a connection group is that both sides … have a connection group so either one could simply create a new connection … from the group and send data?

That’s one reasonable approach.

QUIC allows for many different design choices. It’s up to you to match those choices to your specific requirements.

Is my thinking correct?

It’s best to separate tunnels and streams in this setup. I think it makes sense to have limit yourself to a single tunnel between any given pair of peers. This is exactly like the TCP case.

How you manage streams within that tunnel is very much up to you. Again, QUIC allows for a bunch of designs, and you have to choose one.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

And just like how we determined for the non NWConnectionGroup setup....

We don't want Device A discovering and connecting to Device B and also Device B discovering and connecting to Device A.

We want to have one side (via the comparison of device IDs) discover and connect to the other so we wont have multiple connection groups.


I think for me this is the final detail that a tad fuzzy. Could you walk through an example of three devices, Device A, B and C? Sharing what you would save[1] on each device?

[1] saving things like the connection group, each connection or endpoints?

Again, QUIC allows for a bunch of designs, and you have to choose one.

Could you share the designs?

Could you share the designs?

I can’t enumerate all the possible network design that are possible to implement over QUIC. That’s an unbounded set.

However, my previous post outlined two potential designs:

  • Use a single QUIC stream for command-and-control messages and then use other streams for specific tasks, like transferring large files. The main challenge with this approach is that you have to implement framing on that command-and-control stream.

  • Use a separate QUIC stream for each request/response pair. This is, for example, how HTTP/3 works.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

NWBrowser + NWListener + NWConnection
 
 
Q