Multiplayer RPG Series – Networking layer

By 19 November 2017 Game Development, Networking, Programming No Comments 31 min read
MultiplayerSeries_networking

And here we are with the first, real post about development of this project! I apologize for the huge delay. :)

As most of you certainly noted, we are planning to create a multiplayer game. This means that we need to communicate with other players around us: nobody likes a multiplayer game who has no multiplayer features!
This directly brings us to our next need: we want a way to transfer data between players, to make them know about each other and about their activities.

That’s why I start with the foundation of every multiplayer game: the networking layer.

REQUIREMENTS

But what do we effectively need? What should our networking layer offer to make us happy?
We can’t do a really detailed estimation, because we are in early stages. But we can start with something.

The classic topology for MMO games is client/server, with authority owned by the server: we make no exception. So we just need a way to send a chunk of data from client to the server and back. Nothing more.
To be more specific, we require that our chunk of data can be easily sent and its arrival has to be guaranteed on the other end (when we want/need). We all know that internet is a bad place, where our little network packets often lose their path and nobody is able to find them anymore. :)

I can already hear smartest among you who are already saying: “hey, but TCP is here for this reason”.
And it is right: TCP/IP protocol is here to solve the issue. Under the hood TCP transparently re-sends lost network packets and makes you happy: no packets have been lost, not anymore!

But TCP does a lot of stuff, not only what we said before. It also transfers packets in a guaranteed order (like a stream, the first packet sent has to be the first received one on the other end), it is connection oriented, it can fragment your packets and reassemble them on the other side, it has flow/congestion control, etc. (If interested, you can read and study about TCP starting from here)
A lot of stuff, indeed!

All these features require more resources too: CPU cycles, memory, bandwidth. Plus, these features are not always useful for our objectives.
Indeed, for a business app or for a situation where all these features (or most of them, as we will see when we will build game services instead of game itself) are required we find our Saint Graal with TCP.

But what about games?
Usually games (particularly some genres) don’t require the whole amount of TCP’s features.
As example, we don’t always want to resend a lost packet. This happens when the data contained in that packet is time sensitive and/or sent multiple times in a short interval, like a position update: if a more recent packet already arrived with the new shining data, we don’t care anymore about the lost one who contains the outdated information.
We also don’t always need guaranteed order: the async/event based nature of games doesn’t fit with the concept of “order”. I can move my player while moving stuff in my inventory: I don’t care if the position update arrives after the inventory update. I just care about ordering actions on the same entity, if required.

I can continue with examples, but you certainly got the point. So to make it short and more general: with TCP we are not able to model our networking layer to fit our game’s gameplay architecture, constraints and requirements. And this is not an ignorable point.

On the other side, we have UDP protocol (learn more here) who basically is a connection-less, fire-and-forget protocol. It just carries chunks of data on the other end, if everything goes well. Nothing more.

This can seem unconvenient compared to TCP, but let’s come back to the top of this needed explanation. We said we need just a way to send data and to flag it to guarantee the receiving if we need.
What is the simplest way to achieve this?
TCP does everything we need, but has a huge amount of features we don’t need and we have to workaround someway. We probably have to fight with them to optimize traffic, CPU load, etc. It basically satisfies and conflicts with our objective at the same time. One word: overkilled.
UDP just sends data, we have to implement the reliability: but nothing else. After this, the modified UDP exactly fits what we need.

It is just matter of being able to analyze tools we have based on our needs and being able to choose among them. With all information we collected, probably UDP is the most suitable for our needs. It allows us to have a real control over how packets will be managed (in terms of reliability, etc).
Another reason for me to pick UDP is: I want to implement reliability on top of this protocol as personal challenge, but shhh 😀

TRANSPORT LAYER

So we decided to roll our own UDP-based networking layer. I am so proud of us! 😀

On my side, I’ve already rolled out my own networking library (with all abstractions I need and various utilities, plus a TCP wrapper), so I will not go to re-implement it. I will reuse my own library for this project and I will explain some important concepts from it.
Out of here, you can find a plenty of UDP-based libraries (Lidgren, ENet, Glenn Fiedler’s netcode.io, Hazel, etc): you can use what you prefer or follow me and rolling out your own (or wait for mine eheh :) ).
There are a lot of step-by-step socket tutorials over the internet you can go through: no need to replicate them one more time. Instead, I will explain interesting concepts and logic.

N.B: For those of you who use Unity, it includes a low level networking API (LLAPI) who already implements a reliable UDP transport layer. This can be interesting to explore here too.

So I will focus mainly on:

  • adding reliability to my UDP transport layer in my library
  • adding a new UNET wrapper for my library to make it compatible with all Unity’s supported platforms
  • explaining some design choices I made in my own library

If you are looking for a tutorial for existing networking libraries, this isn’t the right place for you: I am sorry. This is a journey through design decisions behind the development of netcode! :)

MHLab.Networking OVERVIEW

To have a general overview of what I will use, here is a quick review of my library. I built it months and months ago, to support my next plugin on the Unity Asset Store (I already spoke about Pulse.NET here on this blog). It evolved to be a more useful library for me, instead of being built for Pulse it is now built as a more generic low level networking library.
My library is composed by:

  • A Common library, who contains classes and methods for messages handling, buffers, pools, etc
  • A Server library, who represents the server host who host all players and implements all server messages handlers
  • A Client library, who contains the client classes and methods to represent client connection and implements all client messages handlers
  • An Utilities library, who contains useful stuff such as pooled collections, crypting, hashing, file system helpers, etc
  • A Logging library, who allows me to log what happens under the hood

I built this library to target .NET Core 2.0, atleast for the server part (I also built it for .NET Framework 4.6, to host servers in Unity too). The client part, instead, is .NET Framework 4.6 compatible.

Another cool thing is that both Client and Server implementation are extensible: they do nothing on network side without atleast a transport layer pushed on startup. A transport layer, in my library, is represented by the implementation of INetworkSocket. By implementing this interface in your class, you can add various transport layers to your Client/Server.
Example: you want both TCP and UDP transport listening on your server? Implement TcpNetworkSocket and UdpNetworkSocket and push them on server object, you have now two listeners who are waiting for data and that can send data.

Here is how I use my library to create a server with a UDP listener:

_server = NetworkServer.Create();
_server.AddNetworkListener<UdpNetworkSocket>(new IPEndPoint(IPAddress.Any, YourPort));

This detail is important, we will use it for the next paragraph: implementing the UNET transport layer.

UNET TRANSPORT LAYER – SERVER

As I said, the server itself doesn’t perform any network operation. It is just a container for server utilities, buffers, pools, events, etc. and it abstracts network operations. To have a real implementation of a network transport layer, we just create a new implementation of INetworkSocket.
I already have my own UDP implementation of INetworkSocket, so it isn’t of any help here: I will use it for next chapter. Instead, I will start with something from scratch: the UNET implementation.
I am curious to try the Unity LLAPI (and I also want it in my library for Unity compatibility). It also works under WebGL, so it is a plus.

I started with a new class (note that I am using C# for my examples like a sort of pseudo code to explain concepts, but you should be able to extrapolate the logic behind without focusing on the language itself), the UnetNetworkSocket. It has few methods to implement.

public void Start(NetworkServer server, EndPoint endpoint)
{
    _server = server;
    _endpoint = endpoint;

    // Initialization of Unity's Transport Layer.
    NetworkTransport.Init();

    // After it, we must create an Host Topology.
    // It basically takes a configuration about
    // underlayer QoS channels and how many concurrent
    // connections we can host.
    ConnectionConfig config = new ConnectionConfig();
    _reliableChannel = config.AddChannel(QosType.Reliable);
    _unreliableChannel = config.AddChannel(QosType.Unreliable);

    HostTopology topology = new HostTopology(config, ushort.MaxValue);
    // We add the created host to our Transport Layer
    _hostId = NetworkTransport.AddHost(topology, Port);

    LoggerManager.Log(LogLevel.Info,
        "UnetNetwork transport bound to " + endpoint + " and ready to listen for data.");

    Task.Factory.StartNew(ReceiveData, TaskCreationOptions.LongRunning);
}

The Start method is pretty straightforward: it is called when the NetworkServer starts. Parameters represents a reference to the NetworkServer itself and the endpoint where the INetworkSocket will listen on.
You can see how we initialize the Unity’s Transport Layer and we add two QoS channels to the host we created: there are a plenty of QoS (they provide for ordering, reliability, fragmenting, high importance delivery, etc.), you can add them when you need. For the matter of semplicity, we just add Reliable and Unreliable.

As you can notice, the last line starts a new long running task (the thread pool usually starts a dedicated thread for it).

private void ReceiveData()
{
    while (_isRunning)
    {
        int hostId;
        int connectionId;
        int channelId;
        int receivedBytes;
        byte error;
        var data = NetworkTransport.Receive(out hostId, out connectionId, out channelId, _dataBuffer,
            _dataBufferLength,
            out receivedBytes, out error);

        switch (data)
        {
            case NetworkEventType.Nothing:
                break;
            case NetworkEventType.DataEvent:
                var dataBuffer = new byte[receivedBytes];
                Buffer.BlockCopy(_dataBuffer, 0, dataBuffer, 0, receivedBytes);
                HandleData(connectionId, dataBuffer);
                break;
            case NetworkEventType.ConnectEvent:
                HandleNewConnection(connectionId);
                break;
            case NetworkEventType.DisconnectEvent:
                HandleDisconnection(connectionId);
                break;
        }
    }
    NetworkTransport.Shutdown();
}

public void Close()
{
    foreach (var remoteConnection in _connections)
    {
        byte error;
        NetworkTransport.Disconnect(_hostId, remoteConnection.Key, out error);
    }
    _isRunning = false;
    _connections.Clear();
}

Here you can see that we poll the Unity’s Transport Layer to know if any data arrived (new connections, disconnections, data, etc). Some interesting details here:

  • the data buffer is preallocated, so you don’t need to allocate a new buffer everytime you receive something. This is nice, but this is not thread safe. That’s why we create a new buffer for the received data.
  • the infinite loop runs until the flag is set, the Close method just disconnects all connected clients and unset the flag. This allows the Transport Layer to safely shutdown itself when the last received message has been computed.
  • switch cases have a specific order: from the most frequent case to the most unfrequent one. This is to avoid to evaluate frequently other conditions when it is not necessary. The major amount of Receive calls will return a NetworkEventType.Nothing result, so it is the turn of NetworkEventType.DataEvent and – in the end – of Connection/Disconnection events.

The next method we implement is the SendData: after all, our purpose is exchanging data between two peers. The signature allows us to pass data we want to send (as byte array) and the endpoint to send data to. Also, we can specify the QoS level for this data.

public void SendData(EndPoint endpoint, byte[] data, QoS qos = QoS.Unreliable)
{
    var rc = _server.GetRemoteConnection(endpoint);
    var connectionId = rc.GetData<int>(ConnectionIdKey);
    int channel = -1;

    switch (qos)
    {
        case QoS.Reliable:
            channel = _reliableChannel;
            break;
        case QoS.Unreliable:
            channel = _unreliableChannel;
            break;
    }

    if (channel == -1)
    {
        throw new NetworkException("This QoS is not supported.");
    }

    byte error;

    NetworkTransport.Send(_hostId, connectionId, channel, data, data.Length, out error);
    // Check the error here to know about errors, eventually
}

The method here is pretty straightforward: no particular details to explain. We just retrieve the connection based on the passed endpoint and send data over it by calling Unity’s Transport Layer API.

But what about connection/disconnection events? In the SendData method we’ve seen that we need an endpoint to correctly send our data. So we need to store that information when a connection arrives.

private void HandleNewConnection(int connectionId)
{
    string address;
    int port;
    NetworkID network;
    NodeID node;
    byte error;

    NetworkTransport.GetConnectionInfo(_hostId, connectionId, out address, out port, out network, out node,
        out error);

    IPEndPoint ep = new IPEndPoint(IPAddress.Parse(address), port);

    if (!_server.HasRemoteConnection(ep))
    {
        var rc = RemoteConnection.Create(ep);
        rc.ConnectionState = NetworkConnectionState.Connected;
        _server.AddRemoteConnection(rc);
        AddConnection(connectionId, rc);
        rc.StoreData(ConnectionIdKey, connectionId);
        _server.OnNewConnection(rc, new byte[0]);
    }
    else
    {
        RemoteConnection rc = _server.GetRemoteConnection(ep);
        AddConnection(connectionId, rc);
        rc.StoreData(ConnectionIdKey, connectionId);
    }
}

With the call to NetworkTransport.GetConnectionInfo we obtain information about a specific connection. After that, we query the NetworkServer to know if this connection already exists or not so we can deal with it accordingly, with the retrieved endpoint.

Disconnection handler, instead, is just an update of internal state where we held connections IDs, etc. Same for HandleData: it just raises the NetworkServer.OnDataReceived event.

Well, basically we don’t need anything else. Now it is time to connect clients to this server! 😀

UNET TRANSPORT LAYER – CLIENT

Of course the server is useless if we can’t connect clients.
The client implementation is really similar to the server’s one. We create a UnetClientNetworkSocket who implements INetworkSocket interface for client side.

public void Start(NetworkClient client, EndPoint endpoint, byte[] connectionData = null)
{
    _client = client;
    _endpoint = endpoint;

    NetworkTransport.Init();

    ConnectionConfig config = new ConnectionConfig();
    _reliableChannel = config.AddChannel(QosType.Reliable);
    _unreliableChannel = config.AddChannel(QosType.Unreliable);

    HostTopology topology = new HostTopology(config, 1);
    _hostId = NetworkTransport.AddHost(topology, 0);

    // We added the Connect call here!
    byte error;
    _connectionId = NetworkTransport.Connect(_hostId, ((IPEndPoint)_endpoint).Address.ToString(), Port, 0, out error);
    LoggerManager.Log(LogLevel.Info, "UnetNetwork transport bound to " + endpoint + " and ready to listen for data.");

    Task.Factory.StartNew(ReceiveData, TaskCreationOptions.LongRunning);
}

The first method is the Start: it has a different signature compared to server’s one. Just some details to point out on its implementation:

  • The HostTopology object has “1” as maximum connections param: we just want to allow a single connection.
  • The AddHost call has a “0” as port number: this allows the OS to bind the underlayer socket to a free port number
  • We called the NetworkTransport.Connect method to connect to the server.
  • I also start a background long running task who runs ReceiveData method. It is the same method we described earlier.
    I just changed events handling methods as following:

    private void HandleConnection(int connectionId)
    {
        if (connectionId == _connectionId)
        {
            ConnectionState = NetworkConnectionState.Connected;
            _client.OnConnection(_endpoint, null);
        }
    }
    
    private void HandleDisconnection(int connectionId)
    {
        if (connectionId == _connectionId)
        {
            ConnectionState = NetworkConnectionState.Disconnected;
            _client.OnDisconnection(_endpoint);
        }
    }
    
    private void HandleData(int connectionId, byte[] data)
    {
        if (connectionId == _connectionId)
        {
            _client.OnDataReceived(_endpoint, data);
        }
    }
    
    public void Close()
    {
        byte error;
        NetworkTransport.Disconnect(_hostId, _connectionId, out error);
        _isRunning = false;
    }
    

    The interesting thing here is: we compare the connection ID to the result of the NetworkTransport.ConnectEndPoint method (that represents the connection ID of the established connection).
    This is to prevent other connections (eventually) to generate events on our client: server is the only allowed sender here.
    The Close method just disconnects the transport. Nothing complicated.

    As last thing, I also changed a little bit the SendData method:

    public void SendData(byte[] data, QoS qos = QoS.Unreliable)
    {
        if (ConnectionState != NetworkConnectionState.Connected)
            return;
    
        int channel = -1;
    
        switch (qos)
        {
            case QoS.Reliable:
                channel = _reliableChannel;
                break;
            case QoS.Unreliable:
                channel = _unreliableChannel;
                break;
        }
    
        if (channel == -1)
        {
            throw new NetworkException("This QoS is not supported.");
        }
    
        byte error;
    
        NetworkTransport.Send(_hostId, _connectionId, channel, data, data.Length, out error);
    }
    

    TESTING

    And here we are. :)
    We now have the server and the client API: we can proceed to test this system!

    The first test disappointed me so much: Receive method of Unet’s Transport Layer cannot be called outside of main thread. Initially this killed my dreams: we have to compute everything in the game loop, by calling the ReceiveData methods of our INetworkSockets in Update method of MonoBehaviour.
    I don’t like this approach, but I modified anyway the implementation to fit with this new constraint. Here is the final code.

    Server:

    public class UnetNetworkSocket : INetworkSocket
    {
        private const string ConnectionIdKey = "UNET_CONNECTION_ID";
    
        private NetworkServer _server;
        private EndPoint _endpoint;
    
        private int _reliableChannel;
        private int _unreliableChannel;
        private int _hostId;
    
        private readonly byte[] _dataBuffer;
        private const int _dataBufferLength = 8 * 1024;
    
        private readonly Dictionary<int, RemoteConnection> _connections;
    
        public int Port
        {
            get
            {
                if (_endpoint == null)
                    throw new NetworkException("The NetworkSocket isn't bound yet!");
                return ((IPEndPoint) _endpoint).Port;
            }
        }
    
        public UnetNetworkSocket()
        {
            _dataBuffer = new byte[_dataBufferLength];
            _connections = new Dictionary<int, RemoteConnection>();
        }
    
        public void Start(NetworkServer server, EndPoint endpoint)
        {
            _server = server;
            _endpoint = endpoint;
    
            // Initialization of Unity's Transport Layer.
            NetworkTransport.Init();
    
            // After it, we must create an Host Topology.
            // It basically takes a configuration about
            // underlayer QoS channels and how many concurrent
            // connections we can host.
            ConnectionConfig config = new ConnectionConfig();
            _reliableChannel = config.AddChannel(QosType.Reliable);
            _unreliableChannel = config.AddChannel(QosType.Unreliable);
    
            HostTopology topology = new HostTopology(config, ushort.MaxValue - 1);
            // We add the created host to our Transport Layer
            _hostId = NetworkTransport.AddHost(topology, Port);
    
            LoggerManager.Log(LogLevel.Info,
                "UnetNetwork transport bound to " + endpoint + " and ready to listen for data.");
    
            //Task.Factory.StartNew(ReceiveData, TaskCreationOptions.LongRunning);
        }
    
        public void ReceiveData()
        {
            NetworkEventType data;
            do
            {
                int hostId;
                int connectionId;
                int channelId;
                int receivedBytes;
                byte error;
                data = NetworkTransport.Receive(out hostId, out connectionId, out channelId, _dataBuffer,
                    _dataBufferLength,
                    out receivedBytes, out error);
    
                switch (data)
                {
                    case NetworkEventType.Nothing:
                        break;
                    case NetworkEventType.DataEvent:
                        var dataBuffer = new byte[receivedBytes];
                        Buffer.BlockCopy(_dataBuffer, 0, dataBuffer, 0, receivedBytes);
                        HandleData(connectionId, dataBuffer);
                        break;
                    case NetworkEventType.ConnectEvent:
                        HandleNewConnection(connectionId);
                        break;
                    case NetworkEventType.DisconnectEvent:
                        HandleDisconnection(connectionId);
                        break;
                }
            } while (data != NetworkEventType.Nothing);
        }
    
        private void AddConnection(int connectionId, RemoteConnection rc)
        {
            if (_connections.ContainsKey(connectionId))
            {
                _connections[connectionId] = rc;
            }
            else
            {
                _connections.Add(connectionId, rc);
            }
        }
    
        private void HandleNewConnection(int connectionId)
        {
            string address;
            int port;
            NetworkID network;
            NodeID node;
            byte error;
    
            NetworkTransport.GetConnectionInfo(_hostId, connectionId, out address, out port, out network, out node,
                out error);
    
            IPEndPoint ep = new IPEndPoint(IPAddress.Parse(address), port);
    
            if (!_server.HasRemoteConnection(ep))
            {
                var rc = RemoteConnection.Create(ep);
                rc.ConnectionState = NetworkConnectionState.Connected;
                _server.AddRemoteConnection(rc);
                AddConnection(connectionId, rc);
                rc.StoreData(ConnectionIdKey, connectionId);
                _server.OnNewConnection(rc, new byte[0]);
            }
            else
            {
                RemoteConnection rc = _server.GetRemoteConnection(ep);
                AddConnection(connectionId, rc);
                rc.StoreData(ConnectionIdKey, connectionId);
            }
        }
    
        private void HandleData(int connectionId, byte[] data)
        {
            if (_connections.ContainsKey(connectionId))
            {
                var connection = _connections[connectionId];
                if (_server.HasRemoteConnection(connection.RemoteEndpoint))
                {
                    _server.OnDataReceived(connection, data);
                }
            }
        }
    
        private void HandleDisconnection(int connectionId)
        {
            if (_connections.ContainsKey(connectionId))
            {
                var connection = _connections[connectionId];
                if (_server.HasRemoteConnection(connection.RemoteEndpoint))
                {
                    _server.RemoveRemoteConnection(connection.RemoteEndpoint);
                    _connections.Remove(connectionId);
                    _server.OnDisconnection(connection);
                }
                else
                {
                    _connections.Remove(connectionId);
                }
            }
        }
    
        public void SendData(EndPoint endpoint, byte[] data, QoS qos = QoS.Unreliable)
        {
            var rc = _server.GetRemoteConnection(endpoint);
            var connectionId = rc.GetData<int>(ConnectionIdKey);
            int channel = -1;
    
            switch (qos)
            {
                case QoS.Reliable:
                    channel = _reliableChannel;
                    break;
                case QoS.Unreliable:
                    channel = _unreliableChannel;
                    break;
            }
    
            if (channel == -1)
            {
                throw new NetworkException("This QoS is not supported.");
            }
    
            byte error;
    
            NetworkTransport.Send(_hostId, connectionId, channel, data, data.Length, out error);
        }
    
        public void Dispose()
        {
            Close();
        }
    
        public void Close()
        {
            foreach (var remoteConnection in _connections)
            {
                byte error;
                NetworkTransport.Disconnect(_hostId, remoteConnection.Key, out error);
            }
            NetworkTransport.Shutdown();
            _connections.Clear();
        }
    }
    

    Client:

    public class UnetClientNetworkSocket : INetworkSocket
    {
        private NetworkClient _client;
        private EndPoint _endpoint;
    
        private int _reliableChannel;
        private int _unreliableChannel;
        private int _hostId;
        private int _connectionId;
    
        private readonly byte[] _dataBuffer;
        private const int _dataBufferLength = 8 * 1024;
    
        public NetworkConnectionState ConnectionState { get; private set; } = NetworkConnectionState.NotConnected;
        
        public int Port
        {
            get
            {
                if (_endpoint == null)
                    throw new NetworkException("The NetworkSocket isn't bound yet!");
                return ((IPEndPoint)_endpoint).Port;
            }
        }
    
        public UnetClientNetworkSocket()
        {
            _dataBuffer = new byte[_dataBufferLength];
        }
    
        public void Start(NetworkClient client, EndPoint endpoint, byte[] connectionData = null)
        {
            _client = client;
            _endpoint = endpoint;
    
            // Initialization of Unity's Transport Layer.
            NetworkTransport.Init();
    
            // After it, we must create an Host Topology.
            // It basically takes a configuration about
            // underlayer QoS channels and how many concurrent
            // connections we can host.
            ConnectionConfig config = new ConnectionConfig();
            _reliableChannel = config.AddChannel(QosType.Reliable);
            _unreliableChannel = config.AddChannel(QosType.Unreliable);
    
            HostTopology topology = new HostTopology(config, 1);
            // We add the created host to our Transport Layer
            _hostId = NetworkTransport.AddHost(topology, 0);
    
            byte error;
            _connectionId = NetworkTransport.Connect(_hostId, ((IPEndPoint)_endpoint).Address.ToString(), Port, 0, out error);
            LoggerManager.Log(LogLevel.Info, "UnetNetwork transport bound to " + endpoint + " and ready to listen for data.");
    
            //Task.Factory.StartNew(ReceiveData, TaskCreationOptions.LongRunning);
        }
    
        public void ReceiveData()
        {
            NetworkEventType data;
            do
            {
                int hostId;
                int connectionId;
                int channelId;
                int receivedBytes;
                byte error;
                data = NetworkTransport.Receive(out hostId, out connectionId, out channelId, _dataBuffer,
                    _dataBufferLength,
                    out receivedBytes, out error);
    
                switch (data)
                {
                    case NetworkEventType.Nothing:
                        break;
                    case NetworkEventType.DataEvent:
                        var dataBuffer = new byte[receivedBytes];
                        Buffer.BlockCopy(_dataBuffer, 0, dataBuffer, 0, receivedBytes);
                        HandleData(connectionId, dataBuffer);
                        break;
                    case NetworkEventType.ConnectEvent:
                        HandleConnection(connectionId);
                        break;
                    case NetworkEventType.DisconnectEvent:
                        HandleDisconnection(connectionId);
                        break;
                }
            } while (data != NetworkEventType.Nothing);
        }
    
        public void SendData(byte[] data, QoS qos = QoS.Unreliable)
        {
            if (ConnectionState != NetworkConnectionState.Connected)
                return;
    
            int channel = -1;
    
            switch (qos)
            {
                case QoS.Reliable:
                    channel = _reliableChannel;
                    break;
                case QoS.Unreliable:
                    channel = _unreliableChannel;
                    break;
            }
    
            if (channel == -1)
            {
                throw new NetworkException("This QoS is not supported.");
            }
    
            byte error;
    
            NetworkTransport.Send(_hostId, _connectionId, channel, data, data.Length, out error);
        }
    
        private void HandleConnection(int connectionId)
        {
            if (connectionId == _connectionId)
            {
                ConnectionState = NetworkConnectionState.Connected;
                _client.OnConnection(_endpoint, null);
            }
        }
    
        private void HandleDisconnection(int connectionId)
        {
            if (connectionId == _connectionId)
            {
                ConnectionState = NetworkConnectionState.Disconnected;
                _client.OnDisconnection(_endpoint);
            }
        }
    
        private void HandleData(int connectionId, byte[] data)
        {
            if (connectionId == _connectionId)
            {
                _client.OnDataReceived(_endpoint, data);
            }
        }
    
        public void Close()
        {
            byte error;
            NetworkTransport.Disconnect(_hostId, _connectionId, out error);
            NetworkTransport.Shutdown();
        }
    
        public void Dispose()
        {
            Close();
        }
    }
    

    Test in Unity:

    public class NetworkTest : MonoBehaviour
    {
        private const int ServerPort = 4592;
        private NetworkServer _server;
        private NetworkClient _client;
    
        private UnetNetworkSocket _serverSocket;
        private UnetClientNetworkSocket _clientSocket;
    
        void Start()
        {
            _server = NetworkServer.Create();
            _serverSocket = _server.AddNetworkListener<UnetNetworkSocket>(new IPEndPoint(IPAddress.Any, ServerPort));
            _server.NewConnection += ServerNewConnection;
            _server.Disconnection += ServerDisconnection;
            _server.Start();
                
            _client = new NetworkClient();
            _client.ConnectionEvent = (endpoint, data) =>
            {
                Debug.Log("Client connected to the server!");
            };
            _client.DisconnectedEvent = (endpoint, exception) =>
            {
                Debug.Log("Client disconnected from the server!");
            };
            _clientSocket = _client.Connect<UnetClientNetworkSocket>(new IPEndPoint(IPAddress.Loopback, ServerPort));
        }
    
        private void ServerDisconnection(RemoteConnection rc)
        {
            Debug.Log("A client (" + rc.RemoteEndpoint + ") disconnected from the server.");
        }
    
        private void ServerNewConnection(RemoteConnection rc, byte[] buffer)
        {
            Debug.Log("The server received a new connection from: " + rc.RemoteEndpoint);
        }
    
        void Update()
        {
            _serverSocket.ReceiveData();
            _clientSocket.ReceiveData();
        }
        
        void OnDestroy()
        {
            _client.Stop();
            _server.Stop();
        }
    }
    

    So I started to think a little bit about this: if I use a background thread to receive network data, I still have to process operations on Unity components on main thread with a dispatcher. So it isn’t so different. Also, I think that NetworkTransport performs operations on background threads on its own.

    I don’t think I will use this Unet transport layer in a production environment (since I would like to use my own transport layer), but this can be useful to prototype features for now. I will test it, anyway, to see how it performs.

    Well, seems like we finally completed the first technical article of this blog series! I hope you enjoyed it!

    Stay tuned, more articles will come! :)

    Leave a Reply

    Your email address will not be published.