Can NWConnection.receive(minimumIncompleteLength:maximumLength:) return nil data for UDP while connection remains .ready?

I’m using Network Framework with UDP and calling:

connection.receive(minimumIncompleteLength: 1,
                   maximumLength: 1500) { data, context, isComplete, error in
    ... // Some Logic
}

Is it possible for this completion handler to be called with data==nil if I haven't received any kind of error, i.e., error==nil and the connection is still in the .ready state?

Answered by DTS Engineer in 875918022

That’s not the right API to use for receiving datagrams. Rather, use the receiveMessage(completion:) method or one of its variants. That ensures that you get the entire datagram in one hit.

Share and Enjoy

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

That’s not the right API to use for receiving datagrams. Rather, use the receiveMessage(completion:) method or one of its variants. That ensures that you get the entire datagram in one hit.

Share and Enjoy

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

We are building our own custom protocol, and we will be assembling the bytes that we are receiving. We wanted to reduce the kernel overhead of assembling bytes that receiveMessage(completion:) does. The question we have is that while using the receive api can the completion handler can be called even if data==nil?

We wanted to reduce the kernel overhead of assembling bytes that receiveMessage(completion:) does.

I have two things to note about this.

First, there’s no guarantee that your network connections are being run by the kernel. Our platforms have a user-space networking stack and Network framework will choose that over the in-kernel stack in many common cases.

Second, I’m concerned about this concept of “assembling bytes”. In general, UDP datagrams shouldn’t be fragmented and thus there’s no assembling of data at all. A packet arrives, it contains the full UDP datagram, and the content of that datagram is delivered to you.

If you’re building something that does fragment UDP datagrams — that is, it sends datagrams that are larger than the path MTU and thus are subject to IP fragmentation — then it’d be better to work on not doing that rather than trying to worry about optimising this path.

Moreover, even in that case I don’t think the partial datagram will be delivered to you. In the case of UDP, the networking stack collects together all the fragments before it can deliver any part of the datagram [1].

Share and Enjoy

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

[1] In theory this isn’t a requirement, but in practice that’s how it actually works.

I agree with what you stated above, and I apologize for not mentioning that we will prepare the datagram while keeping the maximum MTU in mind. The datagram size will be less than the maximum MTU. I had two queries following that:

  1. What are the pros and cons of using receive and receiveMessage, and when each should be used?
  2. We understand that in the case of receiveMessage, we will only receive nil data if some kind of error has occurred. Howerver if we use receive - in what situations can the data be nil?

The receive(minimumIncompleteLength:maximumLength:completion:) has a lot of options and it works with many different protocols. Given that, coming up with a definitive table of what it will do in every possible case is hard, and I’ve never seen that officially documented in sufficient detail [1].

Given that, I generally stick with protocol-specific recipes:

  • For stream-based protocols, like TCP, I use receive(minimumIncompleteLength:maximumLength:completion:) and then, in the completion handler, I process:
    • The data first, if there is any
    • Then the error, if there is one
  • For UDP, I always use receiveMessage(completion:). This is a great option because UDP datagrams have a limited size that easily fits in memory.
  • For message-based protocols without that convenient limit, things get tricky. You have to use receive(minimumIncompleteLength:maximumLength:completion:) but you then need to worry about the isComplete Boolean and potentially the context. Fortunately, these cases are rare [2].

Notably, this approach is codified by the new NetworkConnection API. In that API the receive methods you have access to vary based on the top-level protocol of the connection, that is, the ApplicationProtocol generic parameter of NetworkChannel. For example:

  • NetworkConnection<TCP> has receive(exactly:) and receive(atLeast:atMost:) via the StreamProtocol conformance.
  • NetworkConnection<UDP> has receive() via the DatagramProtocol conformance. This returns both data and metadata as a tuple.
  • NetworkConnection<WebSocket> can receive full messages but it also supports the startReceive(_:) method for dealing with large messages.

Neat-o!

Share and Enjoy

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

[1] The best I’m aware of is the doc comments in the Swift interface. To see those in Xcode, command-click on the method name in your source file.

[2] One example is WebSocket, but in practice most folks don’t need to worry about that, either because their specific usage of WebSocket doesn’t care about message boundaries or because they always use reasonably short messages.

Can NWConnection.receive(minimumIncompleteLength:maximumLength:) return nil data for UDP while connection remains .ready?
 
 
Q