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