NWConnection issues on iOS 12.2.

I'm implementing UDP based iOS app and using Network framework to do actual communication to remote server. I'm seeing quite a few abnormalities with iOS12.2 which are outlined below:


a) Type of dispatch queue set in NWConnection.start(): It wasn't clear if queue should be serial or even concurrent queue is also fine. nwcat example uses main queue which is serial but wondering whether concurrent queue works as well to speed up things little bit.


On iOS 12.2, when I use concurrent queue, I see that there are two calls to completion handler in quick sucession for `ready` state but there is only one call if I instead use serial queue.


Above behavior is only seen starting with iOS 12.2 and there is always only one call to completion handler for `ready` state on iOS 12.1.1 irrespective of whether serial or concurrent queue is used.


b) UDP datagram size in NWConnection.receive(): Might be related to above but when I invoke NWConnection.receive() after connection state is set to `ready`, I see data size much higher than underlying datagram sizes received by the interface. I verified with wireshark that each datagram is around 1500 bytes with no fragmentation involved while I see datagram size to be around 2500 in some instances in the completion handler.


Isn't data in NWConnection.receive() completion handler at datagram granularity or is it based on available received data irrespective of datagrams? It is at datagram granularity in Network.Extension framework so wondering if it is any different in Network framework.


Sample code below. Line 10 gets called twice on iOS 12.2 with concurrent queue used for NWConnection while it only gets called once in iOS 12.1.1 irrespective of queue type.


    private func setupStateHandler() {
        connection.stateUpdateHandler = { [weak self] (state) in
            guard let strongSelf = self else {
                return
            }

            switch state {
            case .ready:
                strongSelf.canSendAndReceive = true
                strongSelf.receiveDatagrams()
            default:
                strongSelf.canSendAndReceive = false
            }
        }
    }

    private func receiveDatagrams() {
        connection.receive(minimumIncompleteLength: 1, maximumLength: Int(INT32_MAX), completion: { (data, _, isComplete, error) in
            if error != nil {
                return
            }

            if let validData = data, isComplete {
                self.delegate?.didReceiveDatagrams(datagrams: [validData])
            }

            self.receiveDatagrams()
        })
    }

Replies

a) Type of dispatch queue set in

NWConnection.start()

You should always use a serial queue for this sort of thing. That’s not because the framework requires that, but because a concurrent queue will result in behaviour that’ll drive you mad.

Remember, that while all Dispatch queues are FIFO, that only applies up to the point where the block is pulled off the queue and executed. After that point the behaviour diverges:

  • For a serial queue, dispatch won’t start another block until the first block is done, so you get nice sane ordering.

  • For a concurrent queue that’s not the case, so Weird Stuff™ can happen.

Imagine a scenario where Network framework wants to tell you about two events in rapid succession:

  1. The framework enqueues the handler for event A.

  2. The framework enqueues the handler for event B.

  3. Dispatch starts running the block for event A. Something weird happens, and that thread is kicked off the CPU for a few milliseconds.

  4. Now Dispatch starts running the block for event B. Nothing weird happens, so your code runs and processes event B.

  5. Now the thread running your block for event A gets back on the CPU, and you code runs and processes it. Argh, it’s seeing event A after event B!

While on the subject of Dispatch queues, my experience is that a lot of folks think that Dispatch is ‘magic’, that they can throw arbitrary work at Dispatch and it will just deal with it. That is not the case. It’s very easy to tie yourself up in knots if you use Dispatch incorrectly. For more background on this, watch the following WWDC presentations:

The first one describes the concept of “thread explosion”, while the second is an up-to-date summary of the best way to use Dispatch.

b) UDP datagram size in NWConnection.receive()

The

receive(minimumIncompleteLength:maximumLength:completion:)
method will give you all the data that’s available, limited only by
maximumLength
. If you want to preserve message boundaries, call
receiveMessage(completion:)
.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"