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