This specification defines the details of kNet Transport layer over UDP.
kNet over UDP offers the following enhancements over raw UDP datagrams:
The major advantages of using kNet over UDP instead of TCP are the following:
The protocol specification was designed keeping in mind the possibility of implementing a certain set of performance-related features. For more details on this, see the C++ reference implementation.
The image below shows an overview of the structural format of a kNet UDP datagram.
kNet UDP Datagram Structure.
The byte format of a kNet UDP datagram is as follows. Multibyte variables are serialized in little-endian order. To understand the VLE-encoded fields in the headers, see Zero Truncation using Variable-Length Encoding.
u24 bit 23 InOrder flag. If set, this datagram contains InOrder messages.
bit 22 Reliable flag. If set, this datagram is expected to be Acked by the receiver.
bits 0-21 PacketID.
u8 InOrderDeltaCount. (IOD) [Only present if InOrder is set.]
IOD x VLE-1.7/8 InOrderDeltaArray. [Only present if InOrder is set.]
IOD x VLE-1.7/8 InOrderDatagramIndexArray. [Only present if InOrder is set.]
? x .Message. As many times as there are still unparsed bytes left in the datagram.
The byte format of a kNet Message block is as follows.
u16 bit 15 FragmentStart flag. If set, this is the first fragment of a fragmented transfer.
bit 14 Fragment flag. If set, this message is a fragment of a fragmented transfer.
bits 11-13 InOrder code. Specifices the InOrder requirements of this message.
bits 0-10 ContentLength. Specifies the length of the Content block.
u8 SingleInOrderIndex. [Only present if InOrder=6 (110 in base 2).]
u8 MultiInOrderIndexCount. (MIO) [Only present if InOrder=7 (111 in base 2).]
MIO x u8 MultiInOrderIndexArray. [Only present if InOrder=7 (111 in base 2).]
VLE-1.7/1.7/16 FragmentCount. [Only present if FragmentStart is set.]
u8 TransferID. [Only present if Fragment is set or FragmentStart is set.]
VLE-1.7/1.7/16 FragmentNumber. [Only present if Fragment is set and FragmentStart is not set.]
.Content. The length of this field is specified by the ContentLength field.
The byte format of a kNet Content block is as follows.
VLE-1.7/1.7/16 MessageID [Only present if FragmentStart is set or Fragment is not set.] .Payload. The actual data of the message.
All the datagrams transferred through a kNet channel need to follow this datagram format.
To perform proper connection control, the protocol reserves some MessageID values for its own use. All other MessageID values are available for client application to define.
The PingRequest message is sent to detect that the other end is still responding and to estimate the current round-trip-time.
The PingReply message is a response to the PingRequest message.
To request a new inbound datagram send rate limit, the client can issue a FlowControlRequest message.
u16 newDatagramReceiveRate.
Reliable. Out-of-order. May not be fragmented.
The PacketAck message is sent to acknowedge the receival of reliable datagrams.
u24 bits 0-21 PacketID.
bits 22-23 PacketID sequence bits.
u32 PacketID sequence bits.
Unreliable. Out-of-order. May not be fragmented.
To inform the other end that the client is about to finish the session and will not send any more messages, it issues the Disconnect message.
Note the ordering requirements of the messages. The Disconnect message needs to be sent in an In-order datagram since the transport layer must guarantee that all other application datagrams have been processed before the Disconnect message.
The DisconnectAck message is used as a response to the Disconnect message to signal that the connection has now been bilaterally closed.
The DisconnectAck message is marked In-order for the same reason than the Disconnect message.
The ConnectSyn is the first message that a client sends when connecting to a server.
N bytes Application-specific content.
Unreliable. Out-of-order. May not be fragmented.
The server replies to a ConnectSyn with a ConnectSynAck. Depending on the contents of this message, the client interprets this as a succeeded or a failed connection attempt.
N bytes Application-specific content.
Unreliable. Out-of-order. May not be fragmented.
Finally, to signal the server that messaging is working both ways, the client sends the ConnectAck message.
If the application sends any other application-specific message than the ConnectAck message, that message serves as ConnectAck message instead. The purpose of an explicit ConnectAck message is only to avoid connection timeout if the client does not immediately have any other messages to send.
kNet uses the following constant values to specify time intervals and data rates and other behavior-affecting values. These parameters may be changed at will and need not be static, but servers running on a changed set of characteristics should explicitly specify so.
Building a session on top of the connectionless UDP layer is performed using traditional TCP-like three-way handshaking and acknowledged disconnection.
A kNet implementation has to maintain a state variable specifying the current connection state. The connection is expected to operate in the following modes:
The states ConnectionSyn and ConnectionSynAck are collectively referred to as the ConnectionPending state.
To initiate a connection attempt, the client sends a kNet datagram containing a ConnectSyn message. The data contained in this message is arbitrary and up to the application to specify. It can be left empty, or can contain a username or a password. It is up to the server application to parse this data and to either accept or reject the connection attempt. The rest of the communication then proceeds as specified above.
On the server side the host maintains a single listen socket that receives inbound kNet datagrams. It is up to the server application to specify a port number on which the server listens. The server maintains two operational modes:
When the server receives a datagram that corresponds to a new connection endpoint, it operates in the following way:
Disconnecting an established connection (in state ConnectionOK) may be initiated by either party by sending the Disconnect message. After sending this message, no other messages or datagrams may be sent. Upon sending this message, the connection should transition to the ConnectionClosing state.
Finalizing the disconnection attempt (i.e. transitioning to ConnectionClosed) may be performed in three different ways:
The Reliable flag of the datagram header is used to specify whether a datagram is sent as reliable or unreliable. If the flag is set, the other end is expected to acknowledge the receival of the datagram by sending a PacketAck message that contains the PacketID from the datagram header. The connection may send back an acknowledgement right away after receiving a reliable datagram, or it may wait for a while, but no longer than the MaxAckDelay time period, to accumulate several reliable packets and acknowledge them all using a single PacketAck message. By using sequence delta compression, one PacketAck message can acknowledge up to 35 reliable datagrams. A message that is transmitted in a reliable datagram is called a reliable message, and correspondingly, messages transmitted in an unreliable datagram are called unreliable message.
The PingRequest and PingReply messages are used to estimate the Round-Trip-Time (RTT) of the channel, as well as to detect that the connection is still alive. To know which request corresponds to which reply, both messages contain a matching pingID field. The application should maintain an internal counter and increment this field by one for each subsequent ping request that it sends.
The interval at which an implementation sends ping requests is not enforced, but it should reply to any PingRequest messages as fast as possible. A recommended interval period is the PingInterval rate.
Since a connection is deemed lost if no messages have been received through the connection in the time period specified by ConnectionLostTimeout, an implementation should send PingRequest messages as keepalive signals to prevent being disconnected.
Raw UDP datagrams may be received out-of-order and it is up to the client to reorganize them. The stream-oriented TCP protocol handles this transparently to the user. However, in certain conditions this feature can be devastating with respect to network latency, and is seen as a major issue for realtime applications. Minimizing network latency and maintaining a total order at all times are two mutually exclusive goals. Therefore modelling message ordering requirements in a realtime network application can be tricky.
Each kNet datagram is associated with a unique PacketID, which is incremented by one after sending a packet. The client can use this information to reconstruct the original send order of the datagrams. However, the protocol does not enforce a single method for message ordering.
When a connection receives a datagram with several messages, it is required to process the messages in the order they appear in the datagram. For dependencies between messages that were transmitted in different datagrams, kNet proposes the following model. On protocol level, a message can depend on several different messages. This means that each message is associated with a list of (PacketID, MessageIndex) values that specify which messages in previous datagrams should have been applied before the given message can be applied. If a message does not specify any InOrder dependencies, it can be applied out-of-order without regard to other messages. A datagram can contain a mix of both ordered and out-of-order messages.
As an example, the client may implement one of the following patterns for message ordering.
If the datagram contains any messages that have ordering requirements, the datagram header has the InOrder flag set. In this case, the datagram header also stores an array of PacketID values (delta-encoded) that specifies the full list of datagrams that the messages in the current datagram depend on. The array is stored length-prefixed, so the field InOrderDeltaCount specifies the size of this array.
Sending ordered messages has the following limitations:
In the Message block header, the three bits of the InOrderCode field are read most significant bit first and treated in the following manner:
0 This message does not depend on previous messages. 1-5 This message depends on a single message specified by InOrderDeltaArray[InOrderCode-1]. 6 This message depends on a single message specified by InOrderDeltaArray[InOrderIndex]. 7 This message depends on multiple messages specified by InOrderDeltaArray[MultiInOrderArray[x]].
The values of InOrderDeltaArray store PacketID delta values. That is, the actual array of PacketID values that this datagram depends on can be computed with the formula
DependedPacketID[x] = PacketID(current datagram) - InOrderDeltaArray[x] - 1;
To avoid network congestion -related problems, the protocol implements a connection control message called FlowControlRequest that can be used to set the maximum datagram rate that the client is ready to receive. An implementation must track its send rate and honor this limit. If a client continuously receives datagrams at a rate higher than the requested limit, it may close the connection.
Our reference implementation uses the same idea as with TCP. That is, when the line is lossless, the data receive rate grows in linear increments. In the presence of packet loss, a geometric reduction is applied. However, we note that the TCP method is unsuitable for certain uses and different applications may benefit from different flow control methods. Therefore it is open to the application to define the exact flow control algorithm that should be used. The reference implementation provides facilities for replacing the TCP method with custom flow control.
At a minimum, a client is always allowed to send datagrams at MinDatagramRate datagrams/second. When a new connection is established, an initial datagram send rate of InitialDatagramRate datagrams/second is in effect.
Since the UDP datagrams have an MTU limit, the protocol implements a message fragmentation feature that allows large messages to be sent over several datagrams. A long message may be divided into several smaller message fragments which are then sent as if they were ordinary individual messages. The receiving end tracks these fragments and reassembles them to form the original complete message. This process is called a fragmented transfer, and to distinguish between several simultaneous fragmented transfers, each of them is assigned a unique TransferID. A message that is a fragment of a larger message has the Fragment flag set. If the message is the first fragment of the fragmented transfer, it should have the FragmentStart flag set as well.
The TransferID is an arbitrary identifier the size of a byte that is allocated by the sender to identify the messages that comprise the fragmented transfer. All messages that are part of the same fragmented transfer are sent with the same TransferID. When a fragmented transfer is allocated an ID number, that number may be reused for another fragmented transfer only after all the datagrams containing fragments of that transfer have been acknowledged.
The first fragment is identified with the FragmentStart flag. If this flag is set, the message header also contains a FragmentCount field, which reveals the total number of fragments the transfer with the given id will contain. This information is used by the receiver to identify when the transfer is finished.
There are a few restrictions and notes to make:
1.7.1