Handling Connection Approval with Just Unity Transport

Let’s say I want to limit the number of connections to a server. In Netcode for Gameobjects, one would use the Connection Approval feature of the Network Manager. In Unity Transport, when following the example code, it only shows how to accept an arbitrary number of connections but not how to refuse a connection for a reason and send that reason to the client.

My thought is that in the Accept code, you send the client a message and then call Disconnect() on the NetworkConnection object returned by Accept(). The problem I get here is that a null reference exception occurs in the Connect event on the client when it tries to write something to the server. I assume this is because the server has already disconnected and so the writer is never assigned by the begin send.

What is the best way to handle this?

Server Code

using UnityEngine;
using UnityEngine.Assertions;
using Unity.Collections;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Utilities;

public class ServerBehaviour : MonoBehaviour
{
    public int maxConnections = 2;
    public NetworkDriver m_Driver;
    public NetworkPipeline m_ReliablePipeline;
    private NativeList<NetworkConnection> m_Connections;

    private void Start()
    {
        NetworkSettings settings = new NetworkSettings();
        settings.WithReliableStageParameters(windowSize: 32);
        m_Driver = NetworkDriver.Create(settings);
        m_ReliablePipeline = m_Driver.CreatePipeline(typeof(ReliableSequencedPipelineStage));

        NetworkEndPoint endpoint = NetworkEndPoint.AnyIpv4;
        endpoint.Port = 9000;

        if (m_Driver.Bind(endpoint) != 0)
            Debug.Log("Failed to bind to port 9000");
        else
            m_Driver.Listen();

        m_Connections = new NativeList<NetworkConnection>(Allocator.Persistent);
    }

    private void OnDestroy()
    {
        if (m_Driver.IsCreated)
        {
            m_Driver.Dispose();
            m_Connections.Dispose();
        }
    }

    private void Update()
    {
        m_Driver.ScheduleUpdate().Complete();

        // Clean Up Connections
        for (int i = 0; i < m_Connections.Length; i++)
        {
            if (!m_Connections[i].IsCreated)
            {
                m_Connections.RemoveAtSwapBack(i);
                --i;
            }
        }

        // Accept or Reject New Connections
        NetworkConnection c;
        while ((c = m_Driver.Accept()) != default(NetworkConnection))
        {
            if (m_Connections.Length >= maxConnections)
            {
                Debug.Log($"Rejected client {c.InternalId}");
                // Create some kind of encoding for the functionality.
                // In this case, if first byte is > 0, there was an error and don't read any more of the data.
                // A value of 1 could mean connection error due to full lobby
                byte errCode = 1;

                m_Driver.BeginSend(m_ReliablePipeline, c, out var writer);
                writer.WriteByte(errCode);
                m_Driver.EndSend(writer);

                c.Disconnect(m_Driver);
            }
            else
            {
                m_Connections.Add(c);
                //Debug.Log("Accepted a connection");
                Debug.Log($"Connected to client {c.InternalId}");
                m_Driver.BeginSend(m_ReliablePipeline, c, out var writer);
                writer.WriteByte(0);
                writer.WriteUInt(0);
                m_Driver.EndSend(writer);
            }
        }

        // Handle Events
        DataStreamReader stream;
        for (int i = 0; i < m_Connections.Length; i++)
        {
            NetworkEvent.Type cmd;
            while ((cmd = m_Driver.PopEventForConnection(m_Connections[i], out stream)) != NetworkEvent.Type.Empty)
            {
                if (cmd == NetworkEvent.Type.Data)
                {
                    uint number = stream.ReadUInt();

                    Debug.Log($"Got {number} from Client {m_Connections[i].InternalId}, adding + 2 to it.");
                    number += 2;

                    m_Driver.BeginSend(m_ReliablePipeline, m_Connections[i], out var writer);
                    writer.WriteByte(0);
                    writer.WriteUInt(number);
                    m_Driver.EndSend(writer);
                }
                else if (cmd == NetworkEvent.Type.Disconnect)
                {
                    Debug.Log($"Client {m_Connections[i].InternalId} disconnected from server");
                    m_Connections[i] = default(NetworkConnection);
                }
            }
        }
    }
}

Client Event Handling

DataStreamReader stream;
        NetworkEvent.Type cmd;
        while ((cmd = m_Connection.PopEvent(m_Driver, out stream)) != NetworkEvent.Type.Empty)
        {
            if (cmd == NetworkEvent.Type.Connect)
            {
                Debug.Log("We are now connected to the server");

                uint value = 1;
                m_Driver.BeginSend(m_ReliablePipeline, m_Connection, out var writer);
                writer.WriteUInt(value); // Null reference exception when disconnected in Accept() on server
                m_Driver.EndSend(writer);
            }
            else if (cmd == NetworkEvent.Type.Data)
            {
                byte errCode = stream.ReadByte();
                Debug.Log(errCode);
                if (errCode > 0)
                {
                    Debug.Log($"Got error code {errCode}");
                }
                else
                {
                    uint value = stream.ReadUInt();
                    Debug.Log("Got the value = " + value + " back from the server");
                }
            }
            else if (cmd == NetworkEvent.Type.Disconnect)
            {
                Debug.Log("Client got disconnected from server");
                m_Connection = default(NetworkConnection);
            }
        }

Your basic approach is the correct one. To avoid the the exception you should check the return value of BeginSend on your client, and only write the value and make the EndSend call if it returns 0. Here’s an example:

Debug.Log("We are now connected to the server");

if (m_Driver.BeginSend(m_ReliablePipeline, m_Connection, out var writer) == 0)
{
    writer.WriteUInt(42);
    m_Driver.EndSend(writer);
}
else
{
    Debug.Log("Connection was refused by the server.");
}

In general for production code I would recommend checking the return value of calls made to NetworkDriver. Most of them return error codes on failure (for example in your situation I’d expect it to return -3, NetworkStateMismatch).