LobbyServer/LobbyClient/TcpClient.cs

205 lines
6.4 KiB
C#

using LobbyServerDto;
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Lobbies
{
internal class TcpLobbbyClient : IDisposable
{
internal delegate void DataReceivedEventArgs(int bytes, Memory<byte> data);
internal event DataReceivedEventArgs? DataReceived;
internal delegate void DisconnectedEventArgs(bool clean, string error);
internal event DisconnectedEventArgs? Disconnected;
internal delegate void ConnectedEventArgs();
internal event ConnectedEventArgs? Connected;
private const int HeaderSize = 4;
private const int MaxMessageSize = 4096;
private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(10);
private TcpClient? tcpClient;
private NetworkStream? networkStream;
private CancellationTokenSource? cancellationTokenSource = new CancellationTokenSource();
private bool running = false;
private readonly BufferRental bufferRental = new BufferRental(MaxMessageSize);
private async Task ReadExact(NetworkStream stream, Memory<byte> buffer, int length, CancellationToken token)
{
int offset = 0;
while (offset < length)
{
int read = await stream.ReadAsync(buffer.Slice(offset), token);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
}
internal async Task Connect(string host, int port)
{
bool cleanDisconnect = true;
string error = string.Empty;
Task? pingTask = null;
try
{
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested)
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
token.ThrowIfCancellationRequested();
running = true;
tcpClient = new TcpClient();
using (token.Register(() =>
{
try { tcpClient.Close(); } catch { }
}))
{
await tcpClient.ConnectAsync(host, port);
}
networkStream = tcpClient.GetStream();
Connected?.Invoke();
pingTask = Task.Run(() => PingLoop(token), token);
Memory<byte> buffer = new byte[MaxMessageSize];
while (running && !token.IsCancellationRequested)
{
await ReadExact(networkStream, buffer, HeaderSize, token);
int length = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize));
if (length < 0)
throw new InvalidDataException("Negative message length received.");
if (length > MaxMessageSize)
throw new InvalidDataException($"Message too large: {length} > {MaxMessageSize}.");
if (length == 0)
continue;
await ReadExact(networkStream, buffer, length, token);
DataReceived?.Invoke(length, buffer.Slice(0, length));
}
}
catch (OperationCanceledException)
{
cleanDisconnect = true;
}
catch (Exception e)
{
cleanDisconnect = false;
error = e.Message;
}
finally
{
running = false;
try { cancellationTokenSource?.Cancel(); } catch { }
if (pingTask != null)
{
try { await pingTask; } catch { }
}
try { networkStream?.Dispose(); } catch { }
try { tcpClient?.Close(); } catch { }
try { tcpClient?.Dispose(); } catch { }
networkStream = null;
tcpClient = null;
Disconnected?.Invoke(cleanDisconnect, error);
}
}
private async Task PingLoop(CancellationToken token)
{
while (running && !token.IsCancellationRequested)
{
await Task.Delay(PingInterval, token);
if (!running || token.IsCancellationRequested)
break;
await SendPing(token);
}
}
private async Task SendPing(CancellationToken token)
{
if (!running || networkStream == null)
return;
try
{
await networkStream.WriteAsync(BitConverter.GetBytes(0), 0, 4, token);
}
catch
{
}
}
internal async Task Send(byte[] buffer, int offset, int count)
{
byte[] frame = bufferRental.Rent();
try
{
if (!running || networkStream == null || cancellationTokenSource == null)
return;
if (count < 0 || count > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(count));
if (offset < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(offset));
BitConverter.GetBytes(count).CopyTo(frame, 0);
Buffer.BlockCopy(buffer, offset, frame, HeaderSize, count);
await networkStream.WriteAsync(frame, 0, HeaderSize + count, cancellationTokenSource.Token);
}
catch
{
}
finally
{
bufferRental.Return(frame);
}
}
internal void Stop()
{
running = false;
cancellationTokenSource?.Cancel();
}
public void Dispose()
{
running = false;
try { cancellationTokenSource?.Cancel(); } catch { }
try { networkStream?.Dispose(); } catch { }
try { tcpClient?.Close(); } catch { }
try { tcpClient?.Dispose(); } catch { }
try { cancellationTokenSource?.Dispose(); } catch { }
networkStream = null;
tcpClient = null;
cancellationTokenSource = null;
}
}
}