All about the Unity networking transport layer
This blog is the third in a series about our upcoming networking technology known internally as UNET. We want UNET to be a system that all game developers can use to build multiplayer games for any type of game with any number of players as easily as possible. We’ll be launching UNET in the 5.x cycle.
Get a general overview of the UNET system here, or read on to learn about the UNET transport layer foundation!
When we started to design the new network library for Unity, we want to understand what an ideal library would look like. We realized that we have (roughly) two different types of users:
1. Users who want networking tools that will give them an out of the box result with minimal effort (ideally without any effort at all).
2. Users who develop network-centric games and want very flexible and powerful tools.
Based on these two user types, we divided our network library into two different parts: a HLAPI (high-level API) and a LLAPI (low-level API).
If you’re interested in learning more about the Syncvars we use in the high level API you can read more here. The following discussion relates to the low level API and our library design which was based on the following principles:
Performance, performance, performance…
The LLAPI is a thin layer on top of the UDP socket, most of the work is performed in a separate thread (hence LLAPI can be configured to use the main thread only). There’s no dynamic memory allocation and no heavy synchronization (actually most of the library uses memory barrier synchronization with some atomic increment/decrement operation).
If something can be done using C# it should be
We decided to only expose what we felt our users would need to use. Like BSD sockets, the LLAPI supports just one abstraction – exchanging raw binary messages. There are no tpc-like streams, serializers or RPC calls in the LLAPI; only low level messages.
Flexibility and configurability? Yes please…
If you take a look at TCP socket implementation you can find tons of parameters (timeouts, buffer length etc.) which you can change. We chose to to take a similar approach and to allow users to change almost all of our library parameters so they can tune it to their specific needs. Where we faced a choice between simplicity and flexibility we sacrificed simplicity to flexibility.
Nice and easy
We tried to design the LLAPI to resemble the BSD socket API wherever possible.
Network and transport layers
Logically, the UNET low level library is a network protocol stack built on top of the UDP, containing a “network” layer and a “transport” layer. The network layer is used for creating connections between peers, delivering packets and controlling possible flow and congestion. The transport layer works with “messages” belonging to different communication channels:
Channels serve two different purposes, they can separate messages logically and they provide different delivery grants or quality of service.
Channels configuration is a part of configuration procedure, something we’ll discuss in more detail in an upcoming post. For now, lets just consider the configuration part as “My system will contain up to 10 connections, each connection will have 5 channels, where channel 0 will have this type, and channel 1 will have other type and so on”. The last part of this sentence is defined by:
The second parameter is channel number and last is channel type, or channel qos (delivery grant).
UNET (so far) supports the following QOS:
- Unreliable: An unreliable message which can be dropped due to network conditions, or internal buffers overflow, similar to UDP packet. Example: Short Log Messages
- UnreliableFragmented: Maximum packet length is fixed, but sometimes you will probably want to send “big” messages. This channel type will disassemble your message to fragments before sending, and assemble it back before receiving. As this qos is unreliable, delivery is not granted. Example: Long Logs
- UnreliableSequenced: Channel grants delivery order, as this qos is unreliable, the message can be missed. Example: voice, video
- Reliable: Channel grants delivery (or disconnect) but not grant order. Example: Sending Damage
- ReliableFragmented: Same as UnreliableFragmented, but additionally it will grant delivery. Example: Group Damage
- ReliableSequenced: Same as UnreliableSequenced, but additionally it will grant delivery. This QOS is analogous to TCP stream. Example: File Delivery/Patching
- StateUpdate: An unreliable channel type plus channel with this qos will force drop messages which are older than sending/receiving. If when sent, send buffer contains more than one message, only the youngest will send. If the receiving buffer, when reading, contains more than one message only the youngest will be delivered. Example: Sending Position
- AllCostDelivery: Fairly similar to Reliable qos, but there is a difference. The reliable channel will resend undelivered messages based on round trip time value (RTT) which is a dynamic parameter while AllCostDelivery will automatically resend messages after a period of time (configured value). This can be useful for small important messages: “I shot player A in the head” or “Mini-game starts”. Example: Game events such as firing bullets
If you have a use case you feel doesn’t fit conveniently in these categories, we’d love it if you could bring it up in the comments!
Let’s review typical LLAPI function calls:
1. Initialize library
2. Configure network: topology, channels amount, their types, different timeouts and buffer sizes (we discuss this in a later post)
3. Create socket:
This function will open the socket on port 5000 on all network interfaces, and will return an integer value as a descriptor of this socket
4. Make connection to other peer:
This function will send a connection request to “other peer” at address 127.0.0.1/6000. It will return an integer value as a descriptor of this connection for this host. You will receive the connection event when the connection is established or the disconnect event if the connection cannot be established
5. Send message:
This last function will send binary data contained in the buffer via socket described by hostId for peer-described connectionId using channel #1 (in our case this is “reliable channel”, so for this message delivery will be granted.
6. Receiving network events:
For receiving network events, we chose a poll model. User should poll UTransport.Receive() function to be acknowledged about network events. Note that this model is very similar to ordinary select() call with zero timeout. This function receives 4 different events:
- UNETEventType.kConnectEvent - somebody connects to you, or connection requested by UTransport.Connect() call has been successfully established
- UNETEventType.kDisconnectEvent - somebody disconnects with you, or connection requested by UTransport.Connect() call cannot be established for some reason (error code will report what this reason was)
- UNETEventType.kDatatEvent – New data has been received
- UNETEventType.kNothing - Nothing interesting happened
7. Send disconnect request:
This function call will send a disconnect request to a connection defined by connectionId on host defined by hostId. The connection will close immediately and can be re-used in the future.
That’s it for now! Thanks for reading, and don’t forget to check back (or subscribe to our blog) to catch the next post in the UNET series in which we’ll be discussing network topology configuration.