170 lines
7.2 KiB
C#
170 lines
7.2 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace CommonwealthOnline.Server;
|
|
|
|
internal sealed class TcpGameConnection : IGameConnection
|
|
{
|
|
private readonly TcpClient _client;
|
|
private readonly NetworkStream _stream;
|
|
private readonly SemaphoreSlim _sendGate = new(1, 1);
|
|
private int _closed;
|
|
|
|
public TcpGameConnection(TcpClient client)
|
|
{
|
|
_client = client;
|
|
_client.NoDelay = true;
|
|
_stream = client.GetStream();
|
|
RemoteEndpoint = (IPEndPoint)(_client.Client.RemoteEndPoint ?? throw new InvalidOperationException("TCP remote endpoint missing"));
|
|
ConnectionKey = $"tcp:{RemoteEndpoint.Address}:{RemoteEndpoint.Port}:{Guid.NewGuid():N}";
|
|
}
|
|
|
|
public string ConnectionKey { get; }
|
|
public string TransportName => "tcp";
|
|
public IPEndPoint RemoteEndpoint { get; }
|
|
public bool IsClosed => Volatile.Read(ref _closed) != 0;
|
|
internal NetworkStream Stream => _stream;
|
|
|
|
public async ValueTask<SendOutcome> SendAsync(EncodedPacket packet, CancellationToken cancellationToken = default)
|
|
{
|
|
if (IsClosed) return SendOutcome.NotConnected;
|
|
if (packet.Payload.Length > ProtocolConstants.MaxMessageBytes) return SendOutcome.TooLarge;
|
|
if (packet.Payload.AsSpan().IndexOf((byte)'\n') >= 0 || packet.Payload.AsSpan().IndexOf((byte)'\r') >= 0) return SendOutcome.Error;
|
|
await _sendGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (IsClosed) return SendOutcome.NotConnected;
|
|
await _stream.WriteAsync(packet.Payload, cancellationToken).ConfigureAwait(false);
|
|
await _stream.WriteAsync(new byte[] { (byte)'\n' }, cancellationToken).ConfigureAwait(false);
|
|
return SendOutcome.Sent;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or SocketException or ObjectDisposedException)
|
|
{
|
|
return SendOutcome.NotConnected;
|
|
}
|
|
finally { _sendGate.Release(); }
|
|
}
|
|
|
|
public ValueTask DisconnectAsync(int reason, string debug)
|
|
{
|
|
if (Interlocked.Exchange(ref _closed, 1) != 0) return ValueTask.CompletedTask;
|
|
try { _client.Client.Shutdown(SocketShutdown.Both); } catch { }
|
|
try { _client.Close(); } catch { }
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await DisconnectAsync(0, "dispose");
|
|
_sendGate.Dispose();
|
|
_stream.Dispose();
|
|
_client.Dispose();
|
|
}
|
|
}
|
|
|
|
internal sealed class TcpServerTransport : IAsyncDisposable
|
|
{
|
|
private readonly ServerOptions _options;
|
|
private readonly IServerIngress _server;
|
|
private readonly CancellationTokenSource _shutdown = new();
|
|
private readonly ConcurrentDictionary<string, Task> _clientTasks = new();
|
|
private TcpListener? _listener;
|
|
private Task? _acceptTask;
|
|
|
|
public TcpServerTransport(ServerOptions options, IServerIngress server) { _options = options; _server = server; }
|
|
|
|
public void Start()
|
|
{
|
|
if (_acceptTask is not null) return;
|
|
if (!ServerOptions.TryResolveIpv4(_options.Host, out var address)) throw new InvalidOperationException($"Could not resolve IPv4 bind host {_options.Host}");
|
|
_listener = new TcpListener(address, _options.Port);
|
|
_listener.Start();
|
|
_acceptTask = Task.Run(() => AcceptLoopAsync(_shutdown.Token));
|
|
_server.Log($"TCP compatibility transport listening on {_options.Host}:{_options.Port}");
|
|
}
|
|
|
|
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
TcpClient client;
|
|
try { client = await _listener!.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); }
|
|
catch (OperationCanceledException) { break; }
|
|
catch (ObjectDisposedException) { break; }
|
|
catch (SocketException ex)
|
|
{
|
|
if (!cancellationToken.IsCancellationRequested) _server.Log($"TCP accept error: {ex.Message}", "warning");
|
|
continue;
|
|
}
|
|
var connection = new TcpGameConnection(client);
|
|
bool accepted;
|
|
try { accepted = await _server.AcceptConnectionAsync(connection, cancellationToken).ConfigureAwait(false); }
|
|
catch (Exception ex)
|
|
{
|
|
_server.Log($"TCP admission failed for {connection.RemoteEndpoint}: {ex.Message}", "warning");
|
|
await connection.DisposeAsync();
|
|
continue;
|
|
}
|
|
if (!accepted) { await connection.DisposeAsync(); continue; }
|
|
var task = RunClientAsync(connection, cancellationToken);
|
|
_clientTasks[connection.ConnectionKey] = task;
|
|
_ = task.ContinueWith(_ => _clientTasks.TryRemove(connection.ConnectionKey, out _), TaskScheduler.Default);
|
|
}
|
|
}
|
|
|
|
private async Task RunClientAsync(TcpGameConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
var readBuffer = new byte[4096];
|
|
using var message = new MemoryStream(4096);
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested && !connection.IsClosed)
|
|
{
|
|
int count;
|
|
try { count = await connection.Stream.ReadAsync(readBuffer, cancellationToken).ConfigureAwait(false); }
|
|
catch (OperationCanceledException) { break; }
|
|
catch (Exception ex) when (ex is IOException or SocketException or ObjectDisposedException) { break; }
|
|
if (count == 0) break;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var value = readBuffer[i];
|
|
if (value == (byte)'\n')
|
|
{
|
|
var data = message.ToArray();
|
|
message.SetLength(0);
|
|
if (data.Length > 0 && data[^1] == (byte)'\r') Array.Resize(ref data, data.Length - 1);
|
|
if (data.Length == 0) continue;
|
|
await _server.HandleMessageAsync(connection, data, cancellationToken).ConfigureAwait(false);
|
|
if (connection.IsClosed) return;
|
|
}
|
|
else
|
|
{
|
|
message.WriteByte(value);
|
|
if (message.Length > ProtocolConstants.MaxMessageBytes)
|
|
{
|
|
await _server.EndSessionForTransportAsync(connection, "packet_too_large", "Packet exceeded maximum line size.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await _server.HandleConnectionClosedAsync(connection).ConfigureAwait(false);
|
|
await connection.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_shutdown.Cancel();
|
|
try { _listener?.Stop(); } catch { }
|
|
if (_acceptTask is not null) { try { await _acceptTask.ConfigureAwait(false); } catch { } }
|
|
var tasks = _clientTasks.Values.ToArray();
|
|
if (tasks.Length > 0) { try { await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(2)); } catch { } }
|
|
_shutdown.Dispose();
|
|
}
|
|
}
|