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()); }}