Sending Data via Bonjour and NWConnection

Using NWBrowser and NWListener I'm trying to send a small package of data from the listener/server to the device.

However the device never receives the actual bytes. It either:

  • gets stuck the preparing state
  • the connection gets reset
  • the data is null and is marked as isComplete = true

The only way I can get the device to receive the data is by calling cancel on the NWConnection on the server/NWListener end.

Here is some sample code I am working with: https://github.com/leogdion/JustBonjour/tree/nwlistener

Is this expected behavior that cancel is required?

Answered by Frameworks Engineer in 794142022
  • gets stuck the preparing state
  • the connection gets reset

That seems a little bit odd, usually these first two things will happen if the server that you're connecting to isn't reachable, isn't listening on the port that you're trying to connect to (could the process have crashed?), or just doesn't accept the incoming connection.

If that happens, your calls to receiveMessage will likely deliver nil data that is marked as complete, along with an error indicating that the connection failed and that's why there's no data (partial or otherwise, it'll try to deliver anything it has at that point).

Is this expected behavior that cancel is required?

In this case, yes!

It helps to think about the concept of a "message" when working with different protocols.

Since you're using .tcp as your parameters, you have a protocol stack using just TCP over IP (with no security). Because TCP is a stream based protocol, there's nothing to tell the other side that you're "done" sending unless you either (a) add a protocol on top or (b) close the connection.

HTTP/1.0, for example, does the latter and sends EOF (a FIN bit on the wire) to let the other side know that the message is complete and that it's okay to handle the data.

In this case, you're telling the connection to accumulate all of that data for you and deliver it all at once when a FIN comes in. In your code, when you cancel the connection on the server (the sender of the data), that causes a FIN to be sent, and you see receiveMessage tell you "all done, here's the whole thing", delivering you the data.

It sounds like that isn't what you're looking for, however, so you need to find some way to delineate those messages.

That could be something like adding a TLV framer (see this sample code for an example) to provide message boundaries on the wire, or it could be something as simple as having a fixed size of this message and always reading exactly that many bytes out of the TCP stream. The framer will be the most future-proof, most likely.

If you wanted to confirm that's what's happening, you could try calling receive (docs) instead of receiveMessage with a minimum length of something smaller than the message you're sending. If this is what's happening, you should see data being delivered without having to cancel the connection.

Note, however, that you also might get partial data delivered and need to accumulate it until isComplete is true; that's why you'd want to add a TLV framer and use receiveMessage so that the accumulation can be done inside the connection and you just need to handle logically "complete" messages in your business logic when you read from the connection.

This is the preferred approach and works unless you need to handle messages that are too large to hold in memory all at once. If that's the case, use receive to read out partial bits of each message and save them to disk / otherwise consume them to make room for the rest of the message. It sounds like, for your app, that's probably not necessary, though.

  • gets stuck the preparing state
  • the connection gets reset

That seems a little bit odd, usually these first two things will happen if the server that you're connecting to isn't reachable, isn't listening on the port that you're trying to connect to (could the process have crashed?), or just doesn't accept the incoming connection.

If that happens, your calls to receiveMessage will likely deliver nil data that is marked as complete, along with an error indicating that the connection failed and that's why there's no data (partial or otherwise, it'll try to deliver anything it has at that point).

Is this expected behavior that cancel is required?

In this case, yes!

It helps to think about the concept of a "message" when working with different protocols.

Since you're using .tcp as your parameters, you have a protocol stack using just TCP over IP (with no security). Because TCP is a stream based protocol, there's nothing to tell the other side that you're "done" sending unless you either (a) add a protocol on top or (b) close the connection.

HTTP/1.0, for example, does the latter and sends EOF (a FIN bit on the wire) to let the other side know that the message is complete and that it's okay to handle the data.

In this case, you're telling the connection to accumulate all of that data for you and deliver it all at once when a FIN comes in. In your code, when you cancel the connection on the server (the sender of the data), that causes a FIN to be sent, and you see receiveMessage tell you "all done, here's the whole thing", delivering you the data.

It sounds like that isn't what you're looking for, however, so you need to find some way to delineate those messages.

That could be something like adding a TLV framer (see this sample code for an example) to provide message boundaries on the wire, or it could be something as simple as having a fixed size of this message and always reading exactly that many bytes out of the TCP stream. The framer will be the most future-proof, most likely.

If you wanted to confirm that's what's happening, you could try calling receive (docs) instead of receiveMessage with a minimum length of something smaller than the message you're sending. If this is what's happening, you should see data being delivered without having to cancel the connection.

Note, however, that you also might get partial data delivered and need to accumulate it until isComplete is true; that's why you'd want to add a TLV framer and use receiveMessage so that the accumulation can be done inside the connection and you just need to handle logically "complete" messages in your business logic when you read from the connection.

This is the preferred approach and works unless you need to handle messages that are too large to hold in memory all at once. If that's the case, use receive to read out partial bits of each message and save them to disk / otherwise consume them to make room for the rest of the message. It sounds like, for your app, that's probably not necessary, though.

Thanks for the reply.

Just to give context I am a full stack Swift developer and I am building a library to ensure my development devices (iPhone, Apple Watch, etc...) can find my development Swift Server (Vapor, Hummingbird, etc...)

https://github.com/brightdigit/Sublimation

I have successfully been able to transition from other solutions to using Bonjour to advertise the ip address of the server using NWTxtRecord.

My next goal was to use SwiftNIO:

https://swiftpackageindex.com/apple/swift-nio-transport-services/1.20.0/documentation/niotransportservices/niotsconnectionbootstrap

as opposed to NWListener. It was using that which I ran into the previous issues mentioned (never getting a complete message). I then tried it as you see in the test code and noticed the requirement for using cancel. These messages are typically pretty small (encoded Protobuf) around 500 - 2000 bytes.

For both NWListener and SwiftNIO:

  • In no cases did the server crash, in fact the server would log correct that it did send the message.
  • I did try using just receive and I had the same issue.

and now for the bad news which makes this all mute....

The Apple Watch cannot using the Networking API.

This renders the whole argument which results in me going to back to using NWListener only via NWTXTRecord.

Again thanks for the reply if I'm missing something, let me know.

Sending Data via Bonjour and NWConnection
 
 
Q