2
\$\begingroup\$

So I'm writing a game emulator and would like some advice after finishing the networking. It is designed to accept multiple connections, and process messages from all of them.

Here is just a brief introduction to the packets and how they are structured. [lengthOfString:short][stringEncodedInUtf8:Byte[]]

Let's start with the NetworkHandler - this class is responsible for accepting new connections and storing them in the collection.

public class NetworkHandler : IDisposable
{
    private readonly TcpListener _listener;
    private readonly IList<NetworkClient> _clients;
    private readonly ClientPacketHandler _packetHandler;

    public NetworkHandler(TcpListener listener, IList<NetworkClient> clients, ClientPacketHandler packetHandler)
    {
        _listener = listener;
        _clients = clients;
        _packetHandler = packetHandler;
    }

    public void StartListener()
    {
        _listener.Start();
    }

    public async Task ListenAsync()
    {
        while (true)
        {
            var tcpClient = await _listener.AcceptTcpClientAsync();
            var networkClient = new NetworkClient(tcpClient, _packetHandler);

            _clients.Add(networkClient);

            networkClient.StartReceiving();
        }
    }

    public void Dispose()
    {
        foreach (var client in _clients)
        {
            client.Dispose();
        }

        _listener.Stop();
    }
}

Then we have the NetworkClient, I made this so NetworkHandler could stay small and to follow SRP - This class handles incoming data from the individual connection (client).

public class NetworkClient
{
    private readonly TcpClient _tcpClient;
    private readonly NetworkStream _networkStream;
    private readonly ClientPacketHandler _packetHandler;

    public NetworkClient(TcpClient tcpClient, ClientPacketHandler packetHandler)
    {
        _tcpClient = tcpClient;
        _networkStream = tcpClient.GetStream();
        _packetHandler = packetHandler;
    }

    public void StartReceiving()
    {
        Task.Run(ProcessDataAsync);
    }

    private async Task ProcessDataAsync()
    {
        while (true)
        {
            using var br = new BinaryReader(new MemoryStream(await GetBinaryDataAsync()));
            var messageLength = BinaryPrimitives.ReadInt32BigEndian(br.ReadBytes(4));
            var packetData = br.ReadBytes(messageLength);

            using var br2 = new BinaryReader(new MemoryStream(packetData));
            var packetId = BinaryPrimitives.ReadInt16BigEndian(br2.ReadBytes(2));

            if (packetId == 26979)
            {
                await WriteToStreamAsync(Encoding.Default.GetBytes("<?xml version=\"1.0\"?>\r\n<!DOCTYPE cross-domain-policy SYSTEM \"/xml/dtds/cross-domain-policy.dtd\">\r\n<cross-domain-policy>\r\n<policy-file-request/><allow-access-from domain=\"*\" to-ports=\"*\" />\r\n</cross-domain-policy>\0)"));
            }
            else
            {
                if (!_packetHandler.TryGetPacket(packetId, out var packet))
                {
                    Console.WriteLine("Unhandled packet: " + packetId);
                    return;
                }

                packet.Process(this, new ClientPacketReader(packetData));
            }
        }
    }

    private async Task<byte[]> GetBinaryDataAsync()
    {
        var buffer = new byte[2048];
        var memoryStream = new MemoryStream();

        var bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length);

        while (bytesRead > 0)
        {
            memoryStream.Write(buffer, 0, buffer.Length);
            bytesRead = await memoryStream.ReadAsync(buffer, 0, buffer.Length);
        }

        return memoryStream.ToArray();
    }

    private async Task WriteToStreamAsync(byte[] data)
    {
        await _networkStream.WriteAsync(data, 0, data.Length);
    }

    public void Dispose()
    {
        _tcpClient.Dispose();
    }
}

ClientPacketHandler - fairly straight forward

public class ClientPacketHandler
{
    private readonly Dictionary<int, IClientPacket> _packets;

    public ClientPacketHandler(Dictionary<int, IClientPacket> packets)
    {
        _packets = packets;
    }

    public bool TryGetPacket(int packetId, out IClientPacket packet)
    {
        return _packets.TryGetValue(packetId, out packet);
    }
}

ClientPackerReader - this will be used to read data from the packet. I feel like this class could be improved by using some built in helper type?

public class ClientPacketReader
{
    private readonly byte[] _packetData;
    private int _packetPosition;

    public ClientPacketReader(byte[] packetData)
    {
        _packetData = packetData ?? new byte[0];
    }

    public string ReadString() => Encoding.Default.GetString(ReadFromLength());

    private byte[] ReadFromLength() => ReadBytes(BinaryPrimitives.ReadInt16BigEndian(ReadBytes(2)));

    private byte[] ReadBytes(int bytes)
    {
        var data = new byte[bytes];

        for (var i = 0; i < bytes; i++)
        {
            data[i] = _packetData[_packetPosition++];
        }

        return data;
    }
}

Lastly I want to show you an example packet file

public class ExamplePacket : IClientPacket
{
    public void Process(NetworkClient client, ClientPacketReader reader)
    {    
        Console.WriteLine("Fetch packet data: " + reader.ReadString());
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ You can always remove your own question using the "delete" link under the question. However, please consider that even though this might not be relevant anymore to you, it might be relevant to some future reader. So unless there are privacy concerns or similar involved, consider leaving the question (and therefore also the answer) visible. \$\endgroup\$
    – Graipher
    Commented Mar 10, 2020 at 11:43

1 Answer 1

2
\$\begingroup\$

I don't have enough reputation to leave a comment, but I highly recommend checking out NexusForever : https://github.com/NexusForever/NexusForever/

It is also a game server emulator, and their implementation seems close to yours. It has been a great resource for me, and I hope it can help you too.

I think your packets should only read the incoming data. Processing can be done in a handler, which takes a Packet and a NetworkClient, so that your packets simply read data. The logic is then processed in another class/method. I say this because I see you pass NetworkClient in "Process". I have had the same type of system before, and it can become very unmaintainable.

Your packet reader class seems fine, in most emulators I've seen, it is always implemented this way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.