kNet Transport over UDP

kNet Transport over UDP

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.

KristalliFormat.png

kNet UDP Datagram Structure.

Datagram Byte Format

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.

UDP Datagram Format.

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.

Message Block Format.

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.

Content Block Format.

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.

Reserved Messages

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.

MessageID 0: PingRequest

u8                 pingID.

Unreliable. Out-of-order. May not be fragmented.

The PingReply message is a response to the PingRequest message.

MessageID 1: PingReply

u8                 pingID.

Unreliable. Out-of-order. May not be fragmented.

To request a new inbound datagram send rate limit, the client can issue a FlowControlRequest message.

MessageID 2: FlowControlRequest

u16                 newDatagramReceiveRate.

Reliable. Out-of-order. May not be fragmented.

The PacketAck message is sent to acknowedge the receival of reliable datagrams.

MessageID 3: PacketAck

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.

MessageID 0x3FFFFFFF: Disconnect

                    (no data)

Unreliable. In-order. May not be fragmented.

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.

MessageID 0x3FFFFFFE: DisconnectAck

                    (no data)

Unreliable. In-order. May not be fragmented.

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.

MessageID 0x3FFFFFFD: ConnectSyn

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.

MessageID 0x3FFFFFFC: ConnectSynAck

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.

MessageID 0x3FFFFFFB: ConnectAck

                    (no data)

Reliable. Out-of-order. May not be fragmented.

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.

Characteristic Values

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.

When a new connection is successfully established, this is the default datagram send and receive rate that both ends should adhere to.
InitialDatagramRate: 15 datagrams/second

A connection is always allowed to send datagrams at this rate, independent of the flow control applied.
MinDatagramRate: 5 datagrams/second

The interval at which we send ping messages.
PingInterval: 5 seconds

The maximum time to wait before acking a packet. If there are enough packets to ack for a full ack message, acking will be performed earlier.
MaxAckDelay: 33 milliseconds

The time counter after which an unacked reliable datagram will be resent. (UDP only)
DatagramTimeOut: 2 seconds

The time interval after which, if we don't get a response to a PingRequest message, the connection is declared lost.
ConnectionLostTimeout: PingInterval * 3

The time interval after which, if we don't get a response to a ConnectSyn or a ConnectSynAck message, the connection is declared lost.
ConnectTimeOut: 10 seconds

The time interval after which, if we don't get a response to a Disconnect message, the connection is declared lost.
DisconnectTimeOut: 5 seconds

The maximum number of times sending a reliable message will be tried until the connection is declared lost.
MaxMessageSendCount: 50

The maximum size for a fragmented transfer message.
MaxUDPMessageFragmentSize: 470 bytes

Session Management

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.

inline_mscgraph_1
Session initialization and teardown sequence.

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.

kNet Server Side Listener Socket

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:

Closing a Session

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:

  1. The connection may wait for the DisconnectAck message, which signals that the other end is not going to send any more messages or datagrams either.
  2. If the DisconnectTimeOut time limit is exceeded, and the connection is not interested in receiving the rest of the data from the other end, or
  3. If a client is in a hurry, for example if it wants to stay responsive for the user, it may choose to skip waiting for the DisconnectAck message altogether and directly transition to ConnectionClosed state. This is applicable only in the case that the application knows it is not interested in the remainder of the data sent by the other end.

Reliable Datagrams

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.

Round-Trip-Time Estimation

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.

InOrder Datagrams and Messages

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:

  1. Messages in a single datagram may only depend on 256 different preceding datagrams. If this limit is reached, the transmission must throttle for a while to wait for an acknowledgement on a previous message to fulfill a dependency.
  2. A reliable message can not depend on an unreliable datagram. This is because if the unreliable message never reaches the destination, the reliable message can never be applied. An unreliable message can depend on a reliable message without problems.

In the Message block header, the three bits of the InOrderCode field are read most significant bit first and treated in the following manner:

InOrderCode field interpretation:

  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;

Flow Control

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.

Fragmented Transfers

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. As specified above, connection control messages may not be sent as fragmented transfers.
  2. There may only be 256 simultaneously ongoing fragmented transfers. If this limit is reached, all the previous fragmented transfers must first be finished.
  3. Any datagram that carries a fragmented transfer message needs to be marked reliable. This is to guarantee that reallocation of TransferID values may be safely done.
  4. If the message transmitted as a fragmented transfer has any ordering requirements, only the first message with the FragmentStart flag set needs to specify these requirements. All the subsequent message fragments may be sent out-of-order to improve performance.
  5. An implementation must be prepared to handle the case that messages with a given TransferID can be received before the first message of that fragmented transfer.
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Defines