NWProtocolFramer for Common Code Across TCP and UDP

Hello,


I am developing an iOS game that will use the Network.framework to communicate between 2-6 iOS devices (hopefully more depending on performance).


I began with studying the Apple Tic-Tac-Toe sample project. Like that project, I started with TCP. One device is the server (the source of truth) and the other devices are clients. The server increments a step, and the clients acknowledge the step. I'm trying to achieve a step rate of about 30 - 100ms I want to compare performance with UDP. (I am fairly new to networking, though I use URLSession regularly.)


In the WWDC 2019 session Advances in Networking, Part 2 at time 31:30 Tommy Pauly talks about Framing Protocols and shows a slide that reads "Use framing protocols to write common code across TCP and UDP transports."


I have not seen an example anywhere of this. Does that mean the GameProtocol class itself can be used with UDP?


class GameProtocol: NWProtocolFramerImplementation {
     // This class is from the Tic-Tac-Toe project
}


I have UDP working in a branch of my code, but it is not using GameProtocol like my TCP branch is.


My TCP send:

let contentContext = NWConnection.ContentContext(identifier: "GameMessage", metadata: [NWProtocolFramer.Message(gameMessageType: .gameMessage)])
connection.send(content: jsonData, contentContext: contentContext, isComplete: true, completion: .idempotent)


My UDP send:

connection.send(content: jsonData, completion: .contentProcessed { error in
if let error = error {
    DDLogError("**ERROR** UDPConnection send | \(error.localizedDescription)")
}
})


So I am interested in knowing more about what was meant by "Use framing protocols to write common code across TCP and UDP transports."


Thanks!

Replies

I believe what Tommy meant is that a common framing protocol definition can be used on both TCP and UDP connections. In his example a common DNS framing protocol was given for usage on top of either TCP or UDP connections. Building upon this idea, the same could be applied to your gaming example when running either TCP or UDP. Let's look at an example:


Here is a standard TLV (Type, Length, Value) protocol that consists of the following definition:


| 4 byte type | 4 byte length | N Byte Value |

|-----------------|-------------------|-------------------|

| Header | Header | Payload |


This is the framing definition that can be used between a UDP or TCP connection.


The header definition can essentially be anything you want, but this just builds on top of the GameProtocol from TicTacToe for illustration.

Let's say our protocol needs to flag a type in the first for bytes. This basically just marks packets at flagged or original.

enum MessageType: UInt32 {
    case original = 0
    case flagged  = 1
    
    func getTypeIdentifier() -> String {
        switch self {
        case .original:
            return "ORIGINAL"
        case .flagged:
            return "FLAGGED"
        }
    }
}


Taking into account the MessageType, here is an extension to set these values.

extension NWProtocolFramer.Message {
    convenience init(type: MessageType) {
        self.init(definition: CustomFrame.definition)
        self.messageType = type
    }
    var messageType: MessageType {
        get {
            if let type = self["messageType"] as? MessageType {
                return type
            } else {
                return .original
            }
        }
        set {
            self["messageType"] = newValue
        }
    }
}


Now, just like the GameProtocol, a MessageHeader is created to build the first 8 bytes of our message. This represents a 4 byte type and 4 byte length. Note, this is almost exactly the same as the GameProtocol.

struct MessageHeader: Codable {
    let type: UInt32
    let length: UInt32


    init(type: UInt32, length: UInt32) {
        self.type = type
        self.length = length
    }
    
    init(_ buffer: UnsafeMutableRawBufferPointer) {
        var tempType: UInt32 = 0
        var tempLength: UInt32 = 0
        
        withUnsafeMutableBytes(of: &tempType) { typePtr in
            typePtr.copyMemory(from: UnsafeRawBufferPointer(start: buffer.baseAddress!.advanced(by: 0),
                                                            count: MemoryLayout.size))
        }
        withUnsafeMutableBytes(of: &tempLength) { lengthPtr in
            lengthPtr.copyMemory(from: UnsafeRawBufferPointer(start: buffer.baseAddress!.advanced(by: MemoryLayout.size),
                                                              count: MemoryLayout.size))
        }
        type = tempType
        length = tempLength
    }


    var encodedData: Data {
        var tempType = type
        var tempLength = length
        // First four bytes (MemoryLayout.size) is set with the type
        // Second four bytes is set with the length
        var dataBuffer = Data(bytes: &tempType, count: MemoryLayout.size)
        dataBuffer += Data(bytes: &tempLength, count: MemoryLayout.size)
        
        return dataBuffer
    }
    static var encodedSize: Int {
        return MemoryLayout.size * 2
    }
}

Now it's time to use your TLV definition on a TCP or UDP connection. First, just like TicTacToe, you need to define your NWProtocolFramerImplementation. This handles reading and writing data from the connection and fitting it to your protocol. Take notice of the handleInput method. This method attempts to read the data being sent to your game by extracting the length from your length header. If this were non-CustomFrame packets that didnt fit into your custom frame, you may run into problems here.

class CustomFrame: NWProtocolFramerImplementation {
    
    static var label: String { return "CustomFramer" }
    static let definition = NWProtocolFramer.Definition(implementation: CustomFrame.self)
    
    required init(framer: NWProtocolFramer.Instance) {}
    
    func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult {
        return .ready
    }
    
    func handleInput(framer: NWProtocolFramer.Instance) -> Int {
        
        while true {
            // The issue here is that sometimes fragmented frames get parsed.
            // To avoid this, attempt to define the size
            var tempHeader: MessageHeader? = nil
            let headerSize = MessageHeader.encodedSize
            let parsed = framer.parseInput(minimumIncompleteLength: 0,
                                           maximumLength: headerSize) { (buffer, isComplete) -> Int in
                guard let buffer = buffer else {
                    return 0
                }
                if buffer.count < headerSize {
                    return 0
                }
                // This may seem strange but what we are doing here is setting
                // the tempHeader and returning a parsed value to allow the code
                // below to executed.  tempHeader is unwrapped and used to get the length/type below.
                // Once the length and type are obtained deliverInputNoCopy can be called to read the
                // length from the header to extract N bytes of the value.
                tempHeader = MessageHeader(buffer)


                return headerSize
            }


            // If you can't parse out a complete header, stop parsing and ask for headerSize more bytes.
            guard parsed, let header = tempHeader else {
                return headerSize
            }
            // Create an object to deliver the message.
            var messageType = MessageType.original
            if let parsedMessageType = MessageType(rawValue: header.type) {
                messageType = parsedMessageType
            }
            let message = NWProtocolFramer.Message(type: messageType)


            // Deliver the body of the message, along with the message object.
            if !framer.deliverInputNoCopy(length: Int(header.length), message: message, isComplete: true) {
                return 0
            }
        }
    }
    
    func handleOutput(framer: NWProtocolFramer.Instance, message: NWProtocolFramer.Message, messageLength: Int, isComplete: Bool) {
        do {
            // Extract the type and flag.
            let type = message.messageType
            let length: UInt32 = UInt32(messageLength)


            // Create a new header using the type, length, and flag
            let header = MessageHeader(type: type.rawValue, length:  UInt32(messageLength))
            // Total length of the header is (header.encodedData.count)
            
            framer.writeOutput(data: header.encodedData)
            
            try framer.writeOutputNoCopy(length: messageLength)
        } catch let error {
            os_log(.debug, log: self.log, "Hit error writing %{public}@", error.localizedDescription)
        }
    }
    
    func wakeup(framer: NWProtocolFramer.Instance) { }
    func stop(framer: NWProtocolFramer.Instance) -> Bool { return true }
    func cleanup(framer: NWProtocolFramer.Instance) { }

}

Now, use your framer on either a TCP or UDP connection with NWConnection. Pay attention to NWParameters setting your new CustomFrame definition. After a TCP or UDP connection is created, start your connection.

class NetworkConnection {
    ...
    private var connection: NWConnection?
    
    // Setup either a plain TCP or UDP conenction.
    convenience init(with host: String, port: String, isTCPTransport: Bool) {
        self.init()
        var params: NWParameters?
        if isTCPTransport {
            let tcpOptions = NWProtocolTCP.Options()
            tcpOptions.enableKeepalive = true
            tcpOptions.keepaliveIdle = 2
            params = NWParameters(tls: nil, tcp: tcpOptions)
        } else {
            let udpOptions = NWProtocolUDP.Options()
            params = NWParameters(dtls: nil, udp: udpOptions)
        }
        
        // Set you new CustomFrame definition.
        let frameOptions = NWProtocolFramer.Options(definition: CustomFrame.definition)
        params?.defaultProtocolStack.applicationProtocols.insert(frameOptions, at: 0)


        guard let endpointPort = NWEndpoint.Port(port) else { return }
        let connectionEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: endpointPort)
        connection = NWConnection(to: connectionEndpoint, using: params!)
    }
    ...
}


Now send and receive data that is formatted to your CustomFrame definition and let handleInput and handleOutput do the heavy lifting for you.

class NetworkConnection {   
    ...

    func sendDataOnConnection(type: MessageType, payload: String) {
        
        guard let data = payload.data(using: .utf8) else { return }


        // Set the type and payload on the outbound message in a context. Works for TCP or UDP.
        let message = NWProtocolFramer.Message(type: type)
        let context = NWConnection.ContentContext(identifier: type.getTypeIdentifier(),
                                                  metadata: [message])


        connection?.send(content: data, contentContext: context, isComplete: true, completion: .contentProcessed( { error in
            // Act upon potential error or success
        })

    }
    
    func receiveIncomingDataOnConnection() {
        // Should be able to handle receiving CustomFrames for UDP or TCP
        self.connection?.receiveMessage { [weak self] (content, context, isComplete, error) in


            if let _ = context?.protocolMetadata(definition: CustomFrame.definition) as? NWProtocolFramer.Message,
                let messageContent = content {
                // Do something with messageContent
            }
            
            if error == nil, !isComplete {
                strongSelf.receiveIncomingDataOnConnection()
            }
        }
    }

    ...
}

Then using the NetworkConnection class to send UDP or TCP data on a separate connection could look like:

var networkConnection: NetworkConnection?
...

networkConnection = NetworkConnection(with: "host", port: "port", isTCPTransport: false)
networkConnection?.startConnection() // Not illustrated.

// After connection is started

let type: MessageType = .flagged
let payload = "TESTINGUDPDATA"
networkConnection?.sendDataOnConnection(type: type, payload: payload)


If you have an echo server on the other end you should see something like:

(TYPE) - Byte 01 => 1 at index: 0 
(TYPE) - Byte 00 => 0 at index: 1 
(TYPE) - Byte 00 => 0 at index: 2 
(TYPE) - Byte 00 => 0 at index: 3 
(LENGTH) - Byte 0E => 14 at index: 4 
(LENGTH) - Byte 00 => 0 at index: 5 
(LENGTH) - Byte 00 => 0 at index: 6 
(LENGTH) - Byte 00 => 0 at index: 7 
(VALUE) - Byte 54 => T at index: 8 
(VALUE) - Byte 45 => E at index: 9 
(VALUE) - Byte 53 => S at index: 10 
(VALUE) - Byte 54 => T at index: 11 
(VALUE) - Byte 49 => I at index: 12 
(VALUE) - Byte 4E => N at index: 13 
(VALUE) - Byte 47 => G at index: 14 
(VALUE) - Byte 55 => U at index: 15 
(VALUE) - Byte 44 => D at index: 16 
(VALUE) - Byte 50 => P at index: 17 
(VALUE) - Byte 44 => D at index: 18 
(VALUE) - Byte 41 => A at index: 19 
(VALUE) - Byte 54 => T at index: 20 
(VALUE) - Byte 41 => A at index: 21 
Server received 22 bytes


Having demonstrated that, there are a few caveats here; keep in mind that this example does not use TLS, and you should use TLS. Second, this example assumes you are only dealing with packets from your CustomFrame definition, and sometimes other packet types need to be accounted for as well.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Thanks for breaking this down Matt! Helped me a tonne!

For those trying to recreate this, I did have to change MemoryLayout.size to MemoryLayout.UInt32 to get it to not crap out at runtime with: error: UnsafeMutableRawBufferPointer.copyMemory source has too many elements

  • No problem, thank you for the update.

Add a Comment