Simplify server architecture.

This commit is contained in:
Ray Koopa 2018-12-25 17:36:36 +01:00
parent 677b9eb9c7
commit bb7d633f21
12 changed files with 222 additions and 221 deletions

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.OnlineWorms.Server
/// <summary> /// <summary>
/// Represents a connection with an Online Worms client which replies to received packets appropriately. /// Represents a connection with an Online Worms client which replies to received packets appropriately.
/// </summary> /// </summary>
internal class Client : PacketHandler internal class Client : GameConnection
{ {
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------

View File

@ -1,26 +0,0 @@
namespace Syroot.Worms.OnlineWorms.Server.Core
{
/// <summary>
/// Represents common mathematical operations.
/// </summary>
internal static class MathFuncs
{
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Returns the nearest power of two bigger than the given <paramref name="value"/>.
/// </summary>
/// <param name="value">The value to get the nearest, bigger power of two for.</param>
/// <returns>The nearest, bigger power of two.</returns>
internal static int NextPowerOfTwo(int value)
{
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return ++value;
}
}
}

View File

@ -1,128 +1,161 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Reflection;
using Syroot.BinaryData; using Syroot.BinaryData;
namespace Syroot.Worms.OnlineWorms.Server.Net namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
/// <summary> /// <summary>
/// Represents a stream of <see cref="Packet"/> instances send between a game client and server. /// Represents a class capable of dispatching received <see cref="Packet"/> instances to a corresponding method.
/// </summary> /// </summary>
internal class GameConnection internal class GameConnection : IDisposable
{ {
// A complete packet consists of the following:
// - ushort id
// - ushort dataSize
// - byte[dataSize] data
// ---- CONSTANTS ---------------------------------------------------------------------------------------------- // ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxDataSize = 2048; private const int _maxDataSize = 2048;
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly Stream _tcpStream; private static readonly Dictionary<Type, Dictionary<Type, MethodInfo>> _connectionClassesCache
private readonly byte[] _receiveBuffer = new byte[_maxDataSize]; = new Dictionary<Type, Dictionary<Type, MethodInfo>>();
private readonly byte[] _sendDataBuffer = new byte[_maxDataSize];
private readonly BinaryStream _writeStream; private readonly Dictionary<Type, MethodInfo> _handlers;
private readonly TcpClient _tcpClient;
private readonly byte[] _receiveBuffer;
private readonly byte[] _sendDataBuffer;
private readonly PacketStream _receiveStream;
private readonly PacketStream _sendStream;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameConnection"/> class, receiving and sending packets from and /// Initializes a new instance of the <see cref="GameConnection"/> class, handling the given
/// to the given <paramref name="tcpClient"/>. /// <paramref name="tcpClient"/>.
/// </summary> /// </summary>
/// <param name="tcpClient">The <see cref="TcpClient"/> to communicate with.</param> /// <param name="tcpClient">The <see cref="TcpClient"/> to communicate with.</param>
internal GameConnection(TcpClient tcpClient) internal GameConnection(TcpClient tcpClient)
{ {
_tcpStream = tcpClient.GetStream(); _tcpClient = tcpClient;
_writeStream = new BinaryStream(new MemoryStream(_sendDataBuffer, 0, _maxDataSize, true), Console.WriteLine($"{_tcpClient.Client.RemoteEndPoint} connected");
encoding: Encoding.GetEncoding(949), stringCoding: StringCoding.Int16CharCount);
_handlers = GetHandlers();
_receiveBuffer = new byte[_maxDataSize];
_receiveBuffer = new byte[_maxDataSize];
_receiveStream = new PacketStream(_tcpClient.GetStream());
_sendStream = new PacketStream(new MemoryStream(_sendDataBuffer, 0, _maxDataSize, true));
} }
// ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- // ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing).
Dispose(true);
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary> /// <summary>
/// Returns a <see cref="Packet"/> read from the stream. If the packet could not be read, <see langword="null"> /// Starts handling packets incoming from this connection and dispatches them to corresponding methods.
/// is returned.
/// </summary> /// </summary>
/// <returns>The read <see cref="Packet"/> instance or <see langword="null"> if the packet could not be read. internal void Listen()
/// </returns> {
internal Packet ReceivePacket() Packet packet;
while ((packet = ReceivePacket()) != null)
HandlePacket(packet);
}
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_tcpClient.Dispose();
_sendStream.Dispose();
}
_disposed = true;
}
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private Dictionary<Type, MethodInfo> GetHandlers()
{
Type classType = GetType();
if (!_connectionClassesCache.TryGetValue(classType, out Dictionary<Type, MethodInfo> handlerMethods))
{
handlerMethods = new Dictionary<Type, MethodInfo>();
// Find all packet handling methods which are methods accepting and returning a specific packet.
foreach (MethodInfo method in classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance))
{
Type inPacket = method.GetParameters().FirstOrDefault()?.ParameterType;
if (typeof(Packet).IsAssignableFrom(inPacket) && typeof(Packet).IsAssignableFrom(method.ReturnType))
handlerMethods.Add(inPacket, method);
}
_connectionClassesCache.Add(classType, handlerMethods);
}
return handlerMethods;
}
private void HandlePacket(Packet inPacket)
{
Type packetType = inPacket.GetType();
Console.WriteLine($"{_tcpClient.Client.RemoteEndPoint} >> {packetType.Name}");
// Invoke the handler and send back any packet resulting from it.
if (_handlers[packetType].Invoke(this, new[] { inPacket }) is Packet outPacket)
{
Console.WriteLine($"{_tcpClient.Client.RemoteEndPoint} << {outPacket.GetType().Name}");
SendPacket(outPacket);
}
}
private Packet ReceivePacket()
{ {
// Receive the raw packet data. // Receive the raw packet data.
ushort id; ushort id;
ushort dataSize; ushort dataSize;
try try
{ {
id = _tcpStream.ReadUInt16(); id = _receiveStream.ReadUInt16();
dataSize = _tcpStream.ReadUInt16(); dataSize = _receiveStream.ReadUInt16();
ReadComplete(_receiveBuffer, 0, dataSize); _receiveStream.ReadAll(_receiveBuffer, 0, dataSize);
} }
catch (IOException) { return null; } // The underlying socket closed. catch (IOException) { return null; } // The underlying socket closed.
catch (ObjectDisposedException) { return null; } // The underlying stream closed. catch (ObjectDisposedException) { return null; } // The underlying stream closed.
// Deserialize and return the packet. // Deserialize and return the packet.
Packet packet = PacketFactory.Create(id); Packet packet = PacketFactory.Create(id);
using (BinaryStream readStream = new BinaryStream(new MemoryStream(_receiveBuffer, 0, dataSize, false), using (PacketStream readStream = new PacketStream(new MemoryStream(_receiveBuffer, 0, dataSize, false)))
encoding: Encoding.GetEncoding(949), stringCoding: StringCoding.Int16CharCount))
{
packet.Deserialize(readStream); packet.Deserialize(readStream);
}
return packet; return packet;
} }
/// <summary> private bool SendPacket(Packet packet)
/// Sends the given <paramref name="packet"/> and returns <see langword="true"/> if the packet was sent
/// successfully.
/// </summary>
/// <param name="packet">The <see cref="Packet"/> to send.</param>
/// <returns><see langword="true"/> when the packet was sent.</returns>
internal bool SendPacket(Packet packet)
{ {
// Serialize the raw packet data. // Serialize the raw packet data.
_writeStream.Position = 0; _sendStream.Position = 0;
packet.Serialize(_writeStream); packet.Serialize(_sendStream);
ushort dataSize = (ushort)_writeStream.Position; ushort dataSize = (ushort)_sendStream.Position;
// Send the data and return success. // Send the data and return success.
try try
{ {
_tcpStream.WriteUInt16(PacketFactory.GetID(packet)); _receiveStream.WriteUInt16(PacketFactory.GetID(packet));
_tcpStream.WriteUInt16(dataSize); _receiveStream.WriteUInt16(dataSize);
_tcpStream.Write(_sendDataBuffer, 0, dataSize); _receiveStream.Write(_sendDataBuffer, 0, dataSize);
return true; return true;
} }
catch (IOException) { return false; } // A network error appeared, and communication should end. catch (IOException) { return false; } // A network error appeared, and communication should end.
catch (ObjectDisposedException) { return false; } // The underlying stream was most apparently closed. catch (ObjectDisposedException) { return false; } // The underlying stream was most apparently closed.
} }
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
/// <summary>
/// Reads <paramref name="count"/> number of bytes to the given <paramref name="buffer"/>, starting at the
/// specified <paramref name="offset"/>, and returns the number of bytes read. Since this method ensures that
/// the requested number of bytes is always read, it always returns <paramref name="count"/>, and otherwise
/// throws an <see cref="IOException"/> if the required bytes could not be read.
/// </summary>
/// <param name="buffer">The byte array to write the read data to.</param>
/// <param name="offset">The offset into <paramref name="buffer"/> at which to start writing data.</param>
/// <param name="count">The number of bytes to read into <paramref name="buffer"/>.</param>
/// <exception cref="ArgumentException">The <paramref name="buffer"/> cannot store the requested number of
/// bytes.</exception>
/// <exception cref="IOException">The requested number of bytes could not be read.</exception>
private void ReadComplete(byte[] buffer, int offset, int count)
{
int totalRead = 0;
while (totalRead < count)
{
// Read returns 0 only when the underlying socket is closed, otherwise it blocks.
int read = _tcpStream.Read(buffer, offset + totalRead, count - totalRead);
if (read == 0)
throw new IOException("The underlying stream has closed, 0 bytes were retrieved.");
totalRead += read;
}
}
} }
} }

View File

@ -1,6 +1,4 @@
using Syroot.BinaryData; namespace Syroot.Worms.OnlineWorms.Server.Net
namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
/// <summary> /// <summary>
/// Represents a packet with an ID specifying its contents. To allow the server to instantiate child packet classes, /// Represents a packet with an ID specifying its contents. To allow the server to instantiate child packet classes,
@ -10,8 +8,8 @@ namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal abstract void Deserialize(BinaryStream stream); internal abstract void Deserialize(PacketStream stream);
internal abstract void Serialize(BinaryStream stream); internal abstract void Serialize(PacketStream stream);
} }
} }

View File

@ -1,100 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
namespace Syroot.Worms.OnlineWorms.Server.Net
{
/// <summary>
/// Represents a class capable of dispatching received <see cref="Packet"/> instances to a corresponding method.
/// </summary>
internal class PacketHandler : IDisposable
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly Dictionary<Type, Dictionary<Type, MethodInfo>> _packetHandlers
= new Dictionary<Type, Dictionary<Type, MethodInfo>>();
private readonly TcpClient _tcpClient;
private readonly Dictionary<Type, MethodInfo> _handlerMethods;
private readonly GameConnection _connection;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal PacketHandler(TcpClient tcpClient)
{
Console.WriteLine($"{tcpClient.Client.RemoteEndPoint} connected");
_tcpClient = tcpClient;
_handlerMethods = GetHandlerMethods();
_connection = new GameConnection(_tcpClient);
}
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing).
Dispose(true);
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Starts handling packets incoming from this client and calls corresponding methods.
/// </summary>
internal void Accept()
{
Packet packet;
while ((packet = _connection.ReceivePacket()) != null)
HandlePacket(packet);
}
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
_tcpClient.Dispose();
_disposed = true;
}
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private Dictionary<Type, MethodInfo> GetHandlerMethods()
{
Type classType = GetType();
if (!_packetHandlers.TryGetValue(classType, out Dictionary<Type, MethodInfo> handlerMethods))
{
handlerMethods = new Dictionary<Type, MethodInfo>();
// Find all packet handling methods which are methods accepting and returning a specific packet.
foreach (MethodInfo method in classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance))
{
Type inPacket = method.GetParameters().FirstOrDefault()?.ParameterType;
if (typeof(Packet).IsAssignableFrom(inPacket) && typeof(Packet).IsAssignableFrom(method.ReturnType))
handlerMethods.Add(inPacket, method);
}
_packetHandlers.Add(classType, handlerMethods);
}
return handlerMethods;
}
private void HandlePacket(Packet inPacket)
{
Type packetType = inPacket.GetType();
Console.WriteLine($"{_tcpClient.Client.RemoteEndPoint} >> {packetType.Name}");
// Invoke the handler and send back any packet resulting from it.
if (_handlerMethods[packetType].Invoke(this, new[] { inPacket }) is Packet outPacket)
{
Console.WriteLine($"{_tcpClient.Client.RemoteEndPoint} << {outPacket.GetType().Name}");
_connection.SendPacket(outPacket);
}
}
}
}

View File

@ -0,0 +1,106 @@
using System.IO;
using System.Text;
using Syroot.BinaryData;
namespace Syroot.Worms.OnlineWorms.Server.Net
{
/// <summary>
/// Represents a stream formatting data for being sent or received from <see cref="Packet"/> instances.
/// </summary>
internal class PacketStream : Stream
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly Encoding _win949Encoding;
private readonly Stream _baseStream;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
static PacketStream()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
_win949Encoding = Encoding.GetEncoding(949);
}
internal PacketStream(Stream baseStream)
{
_baseStream = baseStream;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => _baseStream.CanSeek;
public override bool CanWrite => _baseStream.CanWrite;
public override long Length => _baseStream.Length;
public override long Position
{
get => _baseStream.Position;
set => _baseStream.Position = value;
}
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public override void Flush() => _baseStream.Flush();
public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin);
public override void SetLength(long value) => _baseStream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count);
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
_baseStream.Dispose();
_disposed = true;
base.Dispose(disposing);
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Reads <paramref name="count"/> number of bytes to the given <paramref name="buffer"/>, starting at the
/// specified <paramref name="offset"/>, and returns the number of bytes read. Since this method ensures that
/// the requested number of bytes is always read, it always returns <paramref name="count"/>, and otherwise
/// throws an <see cref="IOException"/> if the required bytes could not be read.
/// </summary>
/// <param name="buffer">The byte array to write the read data to.</param>
/// <param name="offset">The offset into <paramref name="buffer"/> at which to start writing data.</param>
/// <param name="count">The number of bytes to read into <paramref name="buffer"/>.</param>
/// <exception cref="IOException">The requested number of bytes could not be read.</exception>
internal void ReadAll(byte[] buffer, int offset, int count)
{
int totalRead = 0;
while (totalRead < count)
{
// Read returns 0 only when the underlying socket is closed, otherwise it blocks.
int read = _baseStream.Read(buffer, offset + totalRead, count - totalRead);
if (read == 0)
throw new IOException("The underlying stream has closed, 0 bytes were retrieved.");
totalRead += read;
}
}
internal string ReadString()
{
// Strings are word prefixed and 0 termianted.
string value = _baseStream.ReadString(StringCoding.Int16CharCount);
_baseStream.Seek(1);
return value;
}
internal void WriteString(string value)
{
// Strings are word prefixed and 0 termianted.
_baseStream.WriteString(value, StringCoding.Int16CharCount, _win949Encoding);
_baseStream.WriteByte(0);
}
}
}

View File

@ -1,5 +1,4 @@
using System; using System;
using Syroot.BinaryData;
namespace Syroot.Worms.OnlineWorms.Server.Net namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
@ -11,8 +10,8 @@ namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal override void Deserialize(BinaryStream stream) { } internal override void Deserialize(PacketStream stream) { }
internal override void Serialize(BinaryStream stream) => throw new NotImplementedException(); internal override void Serialize(PacketStream stream) => throw new NotImplementedException();
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using Syroot.BinaryData;
namespace Syroot.Worms.OnlineWorms.Server.Net namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
@ -17,9 +16,9 @@ namespace Syroot.Worms.OnlineWorms.Server.Net
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal override void Deserialize(BinaryStream stream) => throw new NotImplementedException(); internal override void Deserialize(PacketStream stream) => throw new NotImplementedException();
internal override void Serialize(BinaryStream stream) internal override void Serialize(PacketStream stream)
{ {
stream.WriteString(Unknown); stream.WriteString(Unknown);
stream.WriteString(Version); stream.WriteString(Version);

View File

@ -15,9 +15,9 @@ namespace Syroot.Worms.OnlineWorms.Server.Net
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal override void Deserialize(BinaryStream stream) => throw new NotImplementedException(); internal override void Deserialize(PacketStream stream) => throw new NotImplementedException();
internal override void Serialize(BinaryStream stream) internal override void Serialize(PacketStream stream)
{ {
stream.WriteBoolean(LoginResult == LoginResult.Success); stream.WriteBoolean(LoginResult == LoginResult.Success);
stream.WriteEnum(LoginResult); stream.WriteEnum(LoginResult);

View File

@ -1,5 +1,4 @@
#if DEBUG #if DEBUG
using Syroot.BinaryData;
namespace Syroot.Worms.OnlineWorms.Server.Net namespace Syroot.Worms.OnlineWorms.Server.Net
{ {
@ -23,13 +22,13 @@ namespace Syroot.Worms.OnlineWorms.Server.Net
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal override void Deserialize(BinaryStream stream) internal override void Deserialize(PacketStream stream)
{ {
Data = new byte[stream.Length]; Data = new byte[stream.Length];
stream.Read(Data, 0, Data.Length); stream.Read(Data, 0, Data.Length);
} }
internal override void Serialize(BinaryStream stream) internal override void Serialize(PacketStream stream)
{ {
stream.Write(Data, 0, Data.Length); stream.Write(Data, 0, Data.Length);
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Text;
namespace Syroot.Worms.OnlineWorms.Server namespace Syroot.Worms.OnlineWorms.Server
{ {
@ -8,13 +7,6 @@ namespace Syroot.Worms.OnlineWorms.Server
/// </summary> /// </summary>
internal class Program internal class Program
{ {
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
static Program()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Main(string[] args) private static void Main(string[] args)

View File

@ -29,12 +29,13 @@ namespace Syroot.Worms.OnlineWorms.Server
tcpListener.Start(); tcpListener.Start();
Console.WriteLine($"Listening on port {port}..."); Console.WriteLine($"Listening on port {port}...");
// Continually accept clients and dispatch them to their listening thread.
while (true) while (true)
{ {
Client client = new Client(tcpListener.AcceptTcpClient(), this); Client client = new Client(tcpListener.AcceptTcpClient(), this);
_clients.Add(client); _clients.Add(client);
Task.Run(client.Accept).ContinueWith(_ => Task.Run(client.Listen).ContinueWith(_ =>
{ {
_clients.Remove(client); _clients.Remove(client);
client.Dispose(); client.Dispose();