I'm writing a socket server for a project I am working on. I'm used to working in .NET FW, but I'm attempting to write this using .NET standard/core. Particularly, I am concerned about managing configuration, test-ability, and DI. Guidance on how to achieve this would be greatly appreciated.
I included the classes that I thought were important. I have the following concerns.
- In .NET framework, configuration is usually accessed statically and not injected like it is done here. With many configuration values, how should I manage configuration in .NET standard? For this reason, I thought I could create factory classes like
PacketFactory.cs
that are built using configuration one time on startup. Is this a good or bad strategy? - I've already written a few unit tests for classes, and I'm noticing that I end up with a lot of interfaces and factories (see point #1) just for testing - I assume this is a good thing, but getting concerned with if it will end up as overkill.
Connection.cs
constructor has a lot of parameters, but I don't think that it has too much responsibility; I can even think of more parameters to add later.- I'm starting to see
Program.cs
(or whatever entry point) is going to have a lot of code just to build up a bunch of objects using config values that ultimately get injected later. Is this OK or expected?
There are of course a lot of other things that need to be improved upon, but as this is my first time writing something really thinking about DI and using IoC, I have doubts about the approaches I'm taking.
Program.cs : exists as a separate project that calls into the networking library.
class Program
{
private static IConfigurationRoot _configuration;
static void Main(string[] args)
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var services = new ServiceCollection();
Program.RegisterNetwork(services);
Program.RegisterLogging(services);
var provider = services.BuildServiceProvider();
var service = provider.GetService<INetworkService>();
service.Start();
while(true)
{
Thread.Sleep(1000);
}
}
private static void RegisterNetwork(IServiceCollection services)
{
var settings = new NetworkSettings();
var section = _configuration.GetSection("Project").GetSection("NetworkSettings");
section.Bind(settings);
// TODO: I would really like to figure out how to avoid double binding.
services.Configure<NetworkSettings>(options => section.Bind(options));
var packetWriter = new PacketFactory(settings.MinPacketSize);
services.AddSingleton(packetWriter.GetType(), packetWriter);
// Setup the cryptographic scheme.
var index = settings.CryptorIndex;
var selection = settings.CryptorKeySelectionType;
var cryptor = new Cryptor(index, selection);
var handler = new BarrackPacketHandler();
var connection = new ConnectionFactory(Log.Logger, handler, packetWriter, cryptor, settings.MaxPacketSize, settings.MinPacketSize);
services.AddSingleton<IPacketHandler>(handler);
services.AddSingleton<ConnectionFactory>(connection);
services.AddSingleton<INetworkService, NetworkService>();
}
private static void RegisterLogging(IServiceCollection services)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(_configuration)
.CreateLogger();
services.AddSingleton(Log.Logger);
}
}
NetworkService.cs
public class NetworkService : INetworkService
{
private ConnectionFactory _connectionFactory;
private NetworkSettings _settings;
private ILogger _logger;
/// <summary>
/// Socket responsible for accepting incoming connection requests.
/// </summary>
private Socket _listener;
private List<Connection> _connections = new List<Connection>();
public NetworkService(IOptions<NetworkSettings> settings, ILogger logger, ConnectionFactory connectionFactory)
{
_settings = settings.Value;
_logger = logger;
_connectionFactory = connectionFactory;
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
public void Start()
{
IPAddress ip;
IPAddress.TryParse(_settings.IPAddress, out ip);
var endpoint = new IPEndPoint(ip, _settings.Port);
_listener.Bind(endpoint);
_listener.Listen(_settings.Backlog);
_listener.BeginAccept(this.AcceptCallback, null);
}
private void AcceptCallback(IAsyncResult result)
{
try
{
var client = _listener.EndAccept(result);
var connection = _connectionFactory.Create(client);
_connections.Add(connection);
}
finally
{
// In any circumstance, the server needs to be able to accept new connections without faulting.
_listener.BeginAccept(this.AcceptCallback, null);
}
}
}
Connection.cs
/// <summary>
/// Represents the state of a remote connection to a single client.
/// </summary>
public class Connection
{
private static ILogger _logger;
/// <summary>
/// Specifies the source class containing packet handling functions.
/// </summary>
private static IPacketHandler _handler;
/// <summary>
/// Used to create packets for reading and writing.
/// </summary>
private static PacketFactory _packetFactory;
/// <summary>
/// Socket to a remote client.
/// </summary>
private Socket _client;
/// <summary>
/// Cryptographic implementation to secure packets.
/// </summary>
private Cryptor _cryptor;
/// <summary>
/// The receive buffer where bytes are written to.
/// </summary>
private byte[] _buffer;
public int BufferSize { get; private set; }
public int MinPacketSize { get; private set; }
public Connection(ILogger logger, IPacketHandler handler, PacketFactory packetFactory, Socket client, Cryptor cryptor, int bufferSize, int minPacketSize)
{
_logger = logger;
_handler = handler;
_packetFactory = packetFactory;
_client = client;
_cryptor = cryptor;
_buffer = new byte[bufferSize];
this.BufferSize = bufferSize;
this.MinPacketSize = minPacketSize;
_client.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, this.ReceiveCallback, null);
}
/// <summary>
/// Callback that is invoked upon receiving packets.
/// </summary>
/// <param name="result"></param>
private void ReceiveCallback(IAsyncResult result)
{
try
{
var bytesRead = _client.EndReceive(result);
// If there aren't any bytes received, then transmission is finished.
if (bytesRead <= 0)
{
return;
}
var cursor = 0;
while (cursor < bytesRead)
{
// Filter out any packets with invalid lengths.
// The smallest packet size + the 16-bit header defining the length.
var remaining = bytesRead - cursor;
if (remaining < this.MinPacketSize + sizeof(short))
{
// TODO: Log
break;
}
// The length of the packet is defined as a short at the beginning.
// Valid lengths should be greater than '0' and complete packets (no fragments).
var length = BitConverter.ToInt16(_buffer, 0);
if (length > remaining || length <= 0)
{
break;
}
var packet = new byte[length];
// This will be the offset into the buffer to read following packets.
cursor += sizeof(short);
Buffer.BlockCopy(_buffer, cursor, packet, 0, length);
cursor += length;
_cryptor.Decrypt(packet, 0, length);
var reader = _packetFactory.CreateReader(packet);
_handler.Handle(this, reader);
}
// Clearing the buffer may not be necessary, but it is done for safety.
Array.Clear(_buffer, 0, _buffer.Length);
_client.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, this.ReceiveCallback, null);
}
catch (Exception e)
{
_logger.Error(e.Message, e);
}
}
}
PacketHandler.cs
/// <summary>
/// Defines methods for handling packets with various opcodes.
/// Implement this class by extending it with methods having the [PacketHandlerAttribute] attribute.
/// </summary>
public abstract class PacketHandler : IPacketHandler
{
/// <summary>
/// Contains a mapping of opcodes to handler functions.
/// </summary>
private static Dictionary<PrimaryOp, PacketHandlerMethod> _handlerMap;
/// <summary>
/// Defines the format for a packet handler function.
/// </summary>
/// <param name="conn"></param>
/// <param name="reader"></param>
private delegate void PacketHandlerMethod(Connection conn, PacketReader reader);
/// <summary>
/// Constructs a handler by registering methods for each packet type.
/// </summary>
public PacketHandler()
{
_handlerMap = new Dictionary<PrimaryOp, PacketHandlerMethod>();
var methods = this.GetType().GetMethods();
foreach (var method in methods)
{
var attributes = method.GetCustomAttributes(typeof(PacketHandlerAttribute), false);
foreach (PacketHandlerAttribute attribute in attributes)
{
var opcode = attribute.Opcode;
if (_handlerMap.ContainsKey(opcode))
{
throw new Exception($"Unable to register handler for {opcode.ToString()} because it has already been registered!");
}
var del = (PacketHandlerMethod)Delegate.CreateDelegate(typeof(PacketHandlerMethod), this, method);
_handlerMap.Add(opcode, del);
}
}
}
/// <summary>
/// Looks up a handler for the specified opcode and runs it.
/// </summary>
/// <param name="reader"></param>
public void Handle(Connection conn, PacketReader reader)
{
var opcode = reader.Opcode;
if (!_handlerMap.ContainsKey(opcode))
{
// TODO: Log it?
return;
}
var handler = _handlerMap[opcode];
if (handler == null)
{
// TODO: log it?
return;
}
handler(conn, reader);
}
}
PacketFactory.cs
public class PacketFactory
{
public readonly int MinPacketSize;
public PacketFactory(int minPacketSize)
{
this.MinPacketSize = minPacketSize;
}
public PacketWriter CreateWriter(PrimaryOp opcode)
{
return new PacketWriter(opcode, this.MinPacketSize);
}
public PacketReader CreateReader(byte[] buffer)
{
return new PacketReader(buffer, this.MinPacketSize);
}
}