Added rooms
This commit is contained in:
parent
06471a7674
commit
9eeabb9f24
|
@ -11,4 +11,10 @@
|
||||||
<ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" />
|
<ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="client.pfx">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -44,10 +44,10 @@ namespace EonaCat.Connections.Client.Example
|
||||||
Protocol = ProtocolType.TCP,
|
Protocol = ProtocolType.TCP,
|
||||||
Host = "127.0.0.1",
|
Host = "127.0.0.1",
|
||||||
Port = 1111,
|
Port = 1111,
|
||||||
UseSsl = false,
|
UseSsl = true,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
AesPassword = "EonaCat.Connections.Password",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
||||||
};
|
};
|
||||||
|
|
||||||
_client = new NetworkClient(config);
|
_client = new NetworkClient(config);
|
||||||
|
@ -60,8 +60,8 @@ namespace EonaCat.Connections.Client.Example
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Connected to server at {e.RemoteEndPoint}");
|
Console.WriteLine($"Connected to server at {e.RemoteEndPoint}");
|
||||||
|
|
||||||
// Send nickname
|
// Set nickname
|
||||||
await _client.SendNicknameAsync("TestUser");
|
await _client.SetNicknameAsync("TestUser");
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
await _client.SendAsync("Hello server!");
|
await _client.SendAsync("Hello server!");
|
||||||
|
|
|
@ -11,4 +11,10 @@
|
||||||
<ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" />
|
<ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="server.pfx">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -38,11 +38,11 @@ namespace EonaCat.Connections.Server.Example
|
||||||
{
|
{
|
||||||
Protocol = ProtocolType.TCP,
|
Protocol = ProtocolType.TCP,
|
||||||
Port = 1111,
|
Port = 1111,
|
||||||
UseSsl = false,
|
UseSsl = true,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
MaxConnections = 100000,
|
MaxConnections = 100000,
|
||||||
AesPassword = "EonaCat.Connections.Password",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss")
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss")
|
||||||
};
|
};
|
||||||
|
|
||||||
_server = new NetworkServer(config);
|
_server = new NetworkServer(config);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
public class ErrorEventArgs : EventArgs
|
public class ErrorEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
|
public string Nickname { get; set; }
|
||||||
public Exception Exception { get; set; }
|
public Exception Exception { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
|
|
@ -48,5 +48,6 @@ namespace EonaCat.Connections.Models
|
||||||
public CancellationTokenSource CancellationToken { get; set; }
|
public CancellationTokenSource CancellationToken { get; set; }
|
||||||
public long BytesSent { get; set; }
|
public long BytesSent { get; set; }
|
||||||
public long BytesReceived { get; set; }
|
public long BytesReceived { get; set; }
|
||||||
|
public SemaphoreSlim SendLock { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,9 +11,6 @@ using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
|
||||||
|
|
||||||
namespace EonaCat.Connections
|
namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
|
||||||
|
|
||||||
public class NetworkClient : IDisposable
|
public class NetworkClient : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
@ -24,7 +21,25 @@ namespace EonaCat.Connections
|
||||||
private CancellationTokenSource _cancellation;
|
private CancellationTokenSource _cancellation;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
|
|
||||||
public bool IsConnected => _isConnected;
|
private readonly object _stateLock = new object();
|
||||||
|
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
private readonly HashSet<string> _joinedRooms = new();
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get { lock (_stateLock)
|
||||||
|
{
|
||||||
|
return _isConnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private set { lock (_stateLock)
|
||||||
|
{
|
||||||
|
_isConnected = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsAutoReconnecting { get; private set; }
|
public bool IsAutoReconnecting { get; private set; }
|
||||||
|
|
||||||
public event EventHandler<ConnectionEventArgs> OnConnected;
|
public event EventHandler<ConnectionEventArgs> OnConnected;
|
||||||
|
@ -41,8 +56,11 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
public async Task ConnectAsync()
|
public async Task ConnectAsync()
|
||||||
{
|
{
|
||||||
_cancellation?.Cancel();
|
lock (_stateLock)
|
||||||
_cancellation = new CancellationTokenSource();
|
{
|
||||||
|
_cancellation?.Cancel();
|
||||||
|
_cancellation = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
if (_config.Protocol == ProtocolType.TCP)
|
if (_config.Protocol == ProtocolType.TCP)
|
||||||
{
|
{
|
||||||
|
@ -58,10 +76,10 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_tcpClient = new TcpClient();
|
var client = new TcpClient();
|
||||||
await _tcpClient.ConnectAsync(_config.Host, _config.Port);
|
await client.ConnectAsync(_config.Host, _config.Port);
|
||||||
|
|
||||||
Stream stream = _tcpClient.GetStream();
|
Stream stream = client.GetStream();
|
||||||
|
|
||||||
if (_config.UseSsl)
|
if (_config.UseSsl)
|
||||||
{
|
{
|
||||||
|
@ -99,8 +117,12 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_stream = stream;
|
lock (_stateLock)
|
||||||
_isConnected = true;
|
{
|
||||||
|
_tcpClient = client;
|
||||||
|
_stream = stream;
|
||||||
|
IsConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
||||||
|
|
||||||
|
@ -108,7 +130,7 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
IsConnected = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect" });
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
}
|
}
|
||||||
|
@ -118,9 +140,14 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_udpClient = new UdpClient();
|
var client = new UdpClient();
|
||||||
_udpClient.Connect(_config.Host, _config.Port);
|
client.Connect(_config.Host, _config.Port);
|
||||||
_isConnected = true;
|
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
_udpClient = client;
|
||||||
|
IsConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
||||||
|
|
||||||
|
@ -128,14 +155,14 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
IsConnected = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect UDP" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect UDP" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReceiveDataAsync(CancellationToken ct)
|
private async Task ReceiveDataAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
while (!ct.IsCancellationRequested && _isConnected)
|
while (!ct.IsCancellationRequested && IsConnected)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -182,7 +209,7 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
IsConnected = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
break;
|
break;
|
||||||
|
@ -210,7 +237,7 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
private async Task ReceiveUdpDataAsync(CancellationToken ct)
|
private async Task ReceiveUdpDataAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
while (!ct.IsCancellationRequested && _isConnected)
|
while (!ct.IsCancellationRequested && IsConnected)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -220,7 +247,7 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
||||||
_isConnected = false;
|
IsConnected = false;
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -258,11 +285,12 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
public async Task SendAsync(byte[] data)
|
public async Task SendAsync(byte[] data)
|
||||||
{
|
{
|
||||||
if (!_isConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _sendLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption && _aesEncryption != null)
|
if (_config.UseAesEncryption && _aesEncryption != null)
|
||||||
|
@ -296,10 +324,52 @@ namespace EonaCat.Connections
|
||||||
var handler = _config.UseAesEncryption ? OnEncryptionError : OnGeneralError;
|
var handler = _config.UseAesEncryption ? OnEncryptionError : OnGeneralError;
|
||||||
handler?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
handler?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sendLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Join a room (server should recognize this command)</summary>
|
||||||
|
public async Task JoinRoomAsync(string roomName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(roomName) || _joinedRooms.Contains(roomName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinedRooms.Add(roomName);
|
||||||
|
await SendAsync($"JOIN_ROOM:{roomName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LeaveRoomAsync(string roomName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(roomName) || !_joinedRooms.Contains(roomName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinedRooms.Remove(roomName);
|
||||||
|
await SendAsync($"LEAVE_ROOM:{roomName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendToRoomAsync(string roomName, string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(roomName) || !_joinedRooms.Contains(roomName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendAsync($"ROOM_MSG:{roomName}:{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> GetJoinedRooms()
|
||||||
|
{
|
||||||
|
return _joinedRooms.ToList().AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendAsync(string message) => await SendAsync(Encoding.UTF8.GetBytes(message));
|
public async Task SendAsync(string message) => await SendAsync(Encoding.UTF8.GetBytes(message));
|
||||||
public async Task SendNicknameAsync(string nickname) => await SendAsync($"NICKNAME:{nickname}");
|
private async Task SendNicknameAsync(string nickname) => await SendAsync($"NICKNAME:{nickname}");
|
||||||
|
|
||||||
private async Task AutoReconnectAsync()
|
private async Task AutoReconnectAsync()
|
||||||
{
|
{
|
||||||
|
@ -309,18 +379,17 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
|
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
|
IsAutoReconnecting = true;
|
||||||
|
|
||||||
while (!_isConnected && (_config.MaxReconnectAttempts == 0 || attempt < _config.MaxReconnectAttempts))
|
while (!IsConnected && (_config.MaxReconnectAttempts == 0 || attempt < _config.MaxReconnectAttempts))
|
||||||
{
|
{
|
||||||
attempt++;
|
attempt++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsAutoReconnecting = true;
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnecting attempt {attempt}" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnecting attempt {attempt}" });
|
||||||
await ConnectAsync();
|
await ConnectAsync();
|
||||||
if (_isConnected)
|
if (IsConnected)
|
||||||
{
|
{
|
||||||
IsAutoReconnecting = false;
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnected after {attempt} attempt(s)" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnected after {attempt} attempt(s)" });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -330,22 +399,43 @@ namespace EonaCat.Connections
|
||||||
await Task.Delay(_config.ReconnectDelayMs);
|
await Task.Delay(_config.ReconnectDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_isConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = "Failed to reconnect" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = "Failed to reconnect" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsAutoReconnecting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string _nickname;
|
||||||
|
public async Task SetNicknameAsync(string nickname)
|
||||||
|
{
|
||||||
|
_nickname = nickname;
|
||||||
|
await SendNicknameAsync(nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Nickname => _nickname;
|
||||||
|
|
||||||
|
|
||||||
public async Task DisconnectAsync()
|
public async Task DisconnectAsync()
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
lock (_stateLock)
|
||||||
_cancellation?.Cancel();
|
{
|
||||||
|
if (!IsConnected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsConnected = false;
|
||||||
|
_cancellation?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
_tcpClient?.Close();
|
_tcpClient?.Close();
|
||||||
_udpClient?.Close();
|
_udpClient?.Close();
|
||||||
_stream?.Dispose();
|
_stream?.Dispose();
|
||||||
_aesEncryption?.Dispose();
|
_aesEncryption?.Dispose();
|
||||||
|
_joinedRooms?.Clear();
|
||||||
|
|
||||||
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self" });
|
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,6 +444,7 @@ namespace EonaCat.Connections
|
||||||
_cancellation?.Cancel();
|
_cancellation?.Cancel();
|
||||||
DisconnectAsync().Wait();
|
DisconnectAsync().Wait();
|
||||||
_cancellation?.Dispose();
|
_cancellation?.Dispose();
|
||||||
|
_sendLock.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,6 @@ using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
|
||||||
|
|
||||||
namespace EonaCat.Connections
|
namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
|
||||||
|
|
||||||
public class NetworkServer : IDisposable
|
public class NetworkServer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
@ -24,6 +21,13 @@ namespace EonaCat.Connections
|
||||||
private UdpClient _udpListener;
|
private UdpClient _udpListener;
|
||||||
private CancellationTokenSource _serverCancellation;
|
private CancellationTokenSource _serverCancellation;
|
||||||
private readonly object _statsLock = new object();
|
private readonly object _statsLock = new object();
|
||||||
|
private readonly object _serverLock = new object();
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, ConcurrentBag<string>> _rooms = new();
|
||||||
|
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> _roomHistory = new();
|
||||||
|
private readonly ConcurrentDictionary<string, string> _roomPasswords = new();
|
||||||
|
private readonly ConcurrentDictionary<string, (int Count, DateTime Timestamp)> _rateLimits = new();
|
||||||
|
private readonly int _maxMessagesPerSecond = 10;
|
||||||
|
|
||||||
public event EventHandler<ConnectionEventArgs> OnConnected;
|
public event EventHandler<ConnectionEventArgs> OnConnected;
|
||||||
public event EventHandler<ConnectionEventArgs> OnConnectedWithNickname;
|
public event EventHandler<ConnectionEventArgs> OnConnectedWithNickname;
|
||||||
|
@ -54,23 +58,47 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
public async Task StartAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
_serverCancellation?.Cancel();
|
lock (_serverLock)
|
||||||
_serverCancellation = new CancellationTokenSource();
|
{
|
||||||
|
if (_serverCancellation != null && !_serverCancellation.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Server is already running
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_config.Protocol == ProtocolType.TCP)
|
_serverCancellation = new CancellationTokenSource();
|
||||||
{
|
|
||||||
await StartTcpServerAsync();
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await StartUdpServerAsync();
|
if (_config.Protocol == ProtocolType.TCP)
|
||||||
|
{
|
||||||
|
await StartTcpServerAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await StartUdpServerAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error starting server" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartTcpServerAsync()
|
private async Task StartTcpServerAsync()
|
||||||
{
|
{
|
||||||
_tcpListener = new TcpListener(IPAddress.Parse(_config.Host), _config.Port);
|
lock (_serverLock)
|
||||||
_tcpListener.Start();
|
{
|
||||||
|
if (_tcpListener != null)
|
||||||
|
{
|
||||||
|
_tcpListener.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_tcpListener = new TcpListener(IPAddress.Parse(_config.Host), _config.Port);
|
||||||
|
_tcpListener.Start();
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine($"TCP Server started on {_config.Host}:{_config.Port}");
|
Console.WriteLine($"TCP Server started on {_config.Host}:{_config.Port}");
|
||||||
|
|
||||||
while (!_serverCancellation.Token.IsCancellationRequested)
|
while (!_serverCancellation.Token.IsCancellationRequested)
|
||||||
|
@ -88,10 +116,54 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly TimeSpan _udpCleanupInterval = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private async Task CleanupInactiveUdpClientsAsync()
|
||||||
|
{
|
||||||
|
while (!_serverCancellation.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var kvp in _clients.ToArray())
|
||||||
|
{
|
||||||
|
var client = kvp.Value;
|
||||||
|
if (client.TcpClient == null && (now - client.LastActive) > TimeSpan.FromMinutes(5))
|
||||||
|
{
|
||||||
|
DisconnectClient(client.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(_udpCleanupInterval, _serverCancellation.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckRateLimit(string clientId)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_rateLimits.TryGetValue(clientId, out var record);
|
||||||
|
if ((now - record.Timestamp).TotalSeconds > 1)
|
||||||
|
{
|
||||||
|
record = (0, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Count++;
|
||||||
|
_rateLimits[clientId] = record;
|
||||||
|
|
||||||
|
return record.Count <= _maxMessagesPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task StartUdpServerAsync()
|
private async Task StartUdpServerAsync()
|
||||||
{
|
{
|
||||||
_udpListener = new UdpClient(_config.Port);
|
lock (_serverLock)
|
||||||
|
{
|
||||||
|
_udpListener?.Close();
|
||||||
|
_udpListener = new UdpClient(_config.Port);
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine($"UDP Server started on {_config.Host}:{_config.Port}");
|
Console.WriteLine($"UDP Server started on {_config.Host}:{_config.Port}");
|
||||||
|
_ = Task.Run(() => CleanupInactiveUdpClientsAsync(), _serverCancellation.Token);
|
||||||
|
|
||||||
while (!_serverCancellation.Token.IsCancellationRequested)
|
while (!_serverCancellation.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
@ -100,7 +172,10 @@ namespace EonaCat.Connections
|
||||||
var result = await _udpListener.ReceiveAsync();
|
var result = await _udpListener.ReceiveAsync();
|
||||||
_ = Task.Run(() => HandleUdpDataAsync(result), _serverCancellation.Token);
|
_ = Task.Run(() => HandleUdpDataAsync(result), _serverCancellation.Token);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) { break; }
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
||||||
|
@ -108,6 +183,7 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleTcpClientAsync(TcpClient tcpClient)
|
private async Task HandleTcpClientAsync(TcpClient tcpClient)
|
||||||
{
|
{
|
||||||
var clientId = Guid.NewGuid().ToString();
|
var clientId = Guid.NewGuid().ToString();
|
||||||
|
@ -118,7 +194,8 @@ namespace EonaCat.Connections
|
||||||
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint,
|
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint,
|
||||||
ConnectedAt = DateTime.UtcNow,
|
ConnectedAt = DateTime.UtcNow,
|
||||||
LastActive = DateTime.UtcNow,
|
LastActive = DateTime.UtcNow,
|
||||||
CancellationToken = new CancellationTokenSource()
|
CancellationToken = new CancellationTokenSource(),
|
||||||
|
SendLock = new SemaphoreSlim(1, 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -147,7 +224,8 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnSslError?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "SSL authentication failed" });
|
var handler = OnSslError;
|
||||||
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "SSL authentication failed" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,7 +234,6 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create AES object
|
|
||||||
client.AesEncryption = Aes.Create();
|
client.AesEncryption = Aes.Create();
|
||||||
client.AesEncryption.KeySize = 256;
|
client.AesEncryption.KeySize = 256;
|
||||||
client.AesEncryption.BlockSize = 128;
|
client.AesEncryption.BlockSize = 128;
|
||||||
|
@ -164,17 +241,12 @@ namespace EonaCat.Connections
|
||||||
client.AesEncryption.Padding = PaddingMode.PKCS7;
|
client.AesEncryption.Padding = PaddingMode.PKCS7;
|
||||||
client.IsEncrypted = true;
|
client.IsEncrypted = true;
|
||||||
|
|
||||||
// Send salt to client to derive key
|
|
||||||
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs
|
var handler = OnEncryptionError;
|
||||||
{
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "AES setup failed" });
|
||||||
ClientId = clientId,
|
|
||||||
Exception = ex,
|
|
||||||
Message = "AES setup failed"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,13 +256,15 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
lock (_statsLock) { _stats.TotalConnections++; }
|
lock (_statsLock) { _stats.TotalConnections++; }
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientId, RemoteEndPoint = client.RemoteEndPoint });
|
var connectedHandler = OnConnected;
|
||||||
|
connectedHandler?.Invoke(this, new ConnectionEventArgs { ClientId = clientId, RemoteEndPoint = client.RemoteEndPoint });
|
||||||
|
|
||||||
await HandleClientCommunicationAsync(client);
|
await HandleClientCommunicationAsync(client);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "Error handling TCP client" });
|
var handler = OnGeneralError;
|
||||||
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "Error handling TCP client" });
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -198,8 +272,6 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
||||||
{
|
{
|
||||||
var clientKey = result.RemoteEndPoint.ToString();
|
var clientKey = result.RemoteEndPoint.ToString();
|
||||||
|
@ -209,11 +281,15 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
Id = clientKey,
|
Id = clientKey,
|
||||||
RemoteEndPoint = result.RemoteEndPoint,
|
RemoteEndPoint = result.RemoteEndPoint,
|
||||||
ConnectedAt = DateTime.UtcNow
|
ConnectedAt = DateTime.UtcNow,
|
||||||
|
SendLock = new SemaphoreSlim(1, 1)
|
||||||
};
|
};
|
||||||
_clients[clientKey] = client;
|
_clients[clientKey] = client;
|
||||||
|
|
||||||
lock (_statsLock) { _stats.TotalConnections++; }
|
lock (_statsLock) { _stats.TotalConnections++; }
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientKey, RemoteEndPoint = result.RemoteEndPoint });
|
|
||||||
|
var handler = OnConnected;
|
||||||
|
handler?.Invoke(this, new ConnectionEventArgs { ClientId = clientKey, RemoteEndPoint = result.RemoteEndPoint });
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProcessReceivedDataAsync(client, result.Buffer);
|
await ProcessReceivedDataAsync(client, result.Buffer);
|
||||||
|
@ -269,7 +345,8 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error reading from client" });
|
var handler = OnGeneralError;
|
||||||
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error reading from client" });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,6 +372,13 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!CheckRateLimit(client.Id))
|
||||||
|
{
|
||||||
|
// Throttle the client
|
||||||
|
await Task.Delay(100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
client.BytesReceived += data.Length;
|
client.BytesReceived += data.Length;
|
||||||
lock (_statsLock)
|
lock (_statsLock)
|
||||||
{
|
{
|
||||||
|
@ -316,7 +400,13 @@ namespace EonaCat.Connections
|
||||||
if (stringData.StartsWith("NICKNAME:"))
|
if (stringData.StartsWith("NICKNAME:"))
|
||||||
{
|
{
|
||||||
client.Nickname = stringData.Substring(9);
|
client.Nickname = stringData.Substring(9);
|
||||||
OnConnectedWithNickname?.Invoke(this, new ConnectionEventArgs { ClientId = client.Id, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
var handler = OnConnectedWithNickname;
|
||||||
|
handler?.Invoke(this, new ConnectionEventArgs
|
||||||
|
{
|
||||||
|
ClientId = client.Id,
|
||||||
|
RemoteEndPoint = client.RemoteEndPoint,
|
||||||
|
Nickname = client.Nickname
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
|
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -324,10 +414,61 @@ namespace EonaCat.Connections
|
||||||
DisconnectClient(client.Id);
|
DisconnectClient(client.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (stringData.StartsWith("JOIN_ROOM:"))
|
||||||
|
{
|
||||||
|
string roomName = stringData.Substring(10);
|
||||||
|
var bag = _rooms.GetOrAdd(roomName, _ => new ConcurrentBag<string>());
|
||||||
|
if (!bag.Contains(client.Id))
|
||||||
|
{
|
||||||
|
bag.Add(client.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (stringData.StartsWith("LEAVE_ROOM:"))
|
||||||
|
{
|
||||||
|
string roomName = stringData.Substring(11);
|
||||||
|
if (_rooms.TryGetValue(roomName, out var bag))
|
||||||
|
{
|
||||||
|
_rooms[roomName] = new ConcurrentBag<string>(bag.Where(id => id != client.Id));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (stringData.StartsWith("ROOM_MSG:"))
|
||||||
|
{
|
||||||
|
var parts = stringData.Substring(9).Split(new[] { ":" }, 2, StringSplitOptions.None);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
string roomName = parts[0];
|
||||||
|
string msg = parts[1];
|
||||||
|
|
||||||
|
if (_rooms.TryGetValue(roomName, out var clients))
|
||||||
|
{
|
||||||
|
// Broadcast to room
|
||||||
|
var tasks = clients.Where(id => _clients.ContainsKey(id))
|
||||||
|
.Select(id => SendDataAsync(_clients[id], Encoding.UTF8.GetBytes($"{client.Nickname}:{msg}")));
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
// Add to room history
|
||||||
|
var history = _roomHistory.GetOrAdd(roomName, _ => new ConcurrentQueue<string>());
|
||||||
|
history.Enqueue($"{client.Nickname}:{msg}");
|
||||||
|
while (history.Count > 100)
|
||||||
|
{
|
||||||
|
history.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await HandleCommand(client, stringData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.LastActive = DateTime.UtcNow;
|
client.LastActive = DateTime.UtcNow;
|
||||||
OnDataReceived?.Invoke(this, new DataReceivedEventArgs
|
var dataHandler = OnDataReceived;
|
||||||
|
dataHandler?.Invoke(this, new DataReceivedEventArgs
|
||||||
{
|
{
|
||||||
ClientId = client.Id,
|
ClientId = client.Id,
|
||||||
Nickname = client.Nickname,
|
Nickname = client.Nickname,
|
||||||
|
@ -340,20 +481,18 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
||||||
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data", Nickname = client.Nickname });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendDataAsync(Connection client, byte[] data)
|
private async Task SendDataAsync(Connection client, byte[] data)
|
||||||
{
|
{
|
||||||
|
await client.SendLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Encrypt if AES is enabled
|
|
||||||
if (client.IsEncrypted && client.AesEncryption != null)
|
if (client.IsEncrypted && client.AesEncryption != null)
|
||||||
{
|
{
|
||||||
data = await AesCryptoHelpers.EncryptDataAsync(data, client.AesEncryption);
|
data = await AesCryptoHelpers.EncryptDataAsync(data, client.AesEncryption);
|
||||||
|
|
||||||
// Prepend 4-byte length (big-endian) for framing
|
|
||||||
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
||||||
if (BitConverter.IsLittleEndian)
|
if (BitConverter.IsLittleEndian)
|
||||||
{
|
{
|
||||||
|
@ -377,7 +516,6 @@ namespace EonaCat.Connections
|
||||||
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
|
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
|
||||||
client.BytesSent += data.Length;
|
client.BytesSent += data.Length;
|
||||||
lock (_statsLock)
|
lock (_statsLock)
|
||||||
{
|
{
|
||||||
|
@ -388,13 +526,86 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
||||||
handler?.Invoke(this, new ErrorEventArgs
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data", Nickname = client.Nickname });
|
||||||
{
|
|
||||||
ClientId = client.Id,
|
|
||||||
Exception = ex,
|
|
||||||
Message = "Error sending data"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
client.SendLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendFileAsync(Connection client, byte[] fileData, int chunkSize = 8192)
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
while (offset < fileData.Length)
|
||||||
|
{
|
||||||
|
int size = Math.Min(chunkSize, fileData.Length - offset);
|
||||||
|
var chunk = new byte[size];
|
||||||
|
Array.Copy(fileData, offset, chunk, 0, size);
|
||||||
|
await SendDataAsync(client, chunk);
|
||||||
|
offset += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMessageToRoomHistory(string roomName, string message)
|
||||||
|
{
|
||||||
|
var queue = _roomHistory.GetOrAdd(roomName, _ => new ConcurrentQueue<string>());
|
||||||
|
queue.Enqueue(message);
|
||||||
|
if (queue.Count > 100)
|
||||||
|
{
|
||||||
|
queue.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetRoomPassword(string roomName, string password)
|
||||||
|
{
|
||||||
|
_roomPasswords[roomName] = password;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool JoinRoomWithPassword(string clientId, string roomName, string password)
|
||||||
|
{
|
||||||
|
if (_roomPasswords.TryGetValue(roomName, out var storedPassword) && storedPassword == password)
|
||||||
|
{
|
||||||
|
JoinRoom(clientId, roomName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IEnumerable<string> GetRoomHistory(string roomName)
|
||||||
|
{
|
||||||
|
if (_roomHistory.TryGetValue(roomName, out var queue))
|
||||||
|
{
|
||||||
|
return queue.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendPrivateMessageAsync(string fromNickname, string toNickname, string message)
|
||||||
|
{
|
||||||
|
var tasks = _clients.Values
|
||||||
|
.Where(c => !string.IsNullOrEmpty(c.Nickname) && c.Nickname.Equals(toNickname, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(c => SendDataAsync(c, Encoding.UTF8.GetBytes($"[PM from {fromNickname}]: {message}")))
|
||||||
|
.ToArray();
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void GetAllClients(out List<Connection> clients)
|
||||||
|
{
|
||||||
|
clients = _clients.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection GetClientById(string clientId)
|
||||||
|
{
|
||||||
|
if (_clients.TryGetValue(clientId, out var client))
|
||||||
|
{
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
return _clients.Values.FirstOrDefault(c => c.Nickname != null && c.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendToClientAsync(string clientId, byte[] data)
|
public async Task SendToClientAsync(string clientId, byte[] data)
|
||||||
|
@ -405,7 +616,6 @@ namespace EonaCat.Connections
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: try nickname
|
|
||||||
foreach (var kvp in _clients)
|
foreach (var kvp in _clients)
|
||||||
{
|
{
|
||||||
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
|
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -438,25 +648,127 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
CleanupClientFromRooms(clientId);
|
||||||
|
|
||||||
client.CancellationToken?.Cancel();
|
client.CancellationToken?.Cancel();
|
||||||
client.TcpClient?.Close();
|
client.TcpClient?.Close();
|
||||||
client.Stream?.Dispose();
|
client.Stream?.Dispose();
|
||||||
client.AesEncryption?.Dispose();
|
client.AesEncryption?.Dispose();
|
||||||
|
|
||||||
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = client.Id, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
foreach (var room in _rooms.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (_rooms.TryGetValue(room, out var bag))
|
||||||
|
{
|
||||||
|
_rooms[room] = new ConcurrentBag<string>(bag.Where(id => id != clientId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = OnDisconnected;
|
||||||
|
handler?.Invoke(this, new ConnectionEventArgs { ClientId = client.Id, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error disconnecting client" });
|
var handler = OnGeneralError;
|
||||||
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error disconnecting client", Nickname = client.Nickname });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void JoinRoom(string clientId, string roomName)
|
||||||
|
{
|
||||||
|
var bag = _rooms.GetOrAdd(roomName, _ => new ConcurrentBag<string>());
|
||||||
|
bag.Add(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LeaveRoom(string clientId, string roomName)
|
||||||
|
{
|
||||||
|
if (_rooms.TryGetValue(roomName, out var bag))
|
||||||
|
{
|
||||||
|
var newBag = new ConcurrentBag<string>(bag.Where(id => id != clientId));
|
||||||
|
_rooms[roomName] = newBag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastToNicknameAsync(string nickname, byte[] data)
|
||||||
|
{
|
||||||
|
var tasks = _clients.Values
|
||||||
|
.Where(c => !string.IsNullOrEmpty(c.Nickname) && c.Nickname.Equals(nickname, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(c => SendDataAsync(c, data))
|
||||||
|
.ToArray();
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastToNicknameAsync(string nickname, string message)
|
||||||
|
{
|
||||||
|
await BroadcastToNicknameAsync(nickname, Encoding.UTF8.GetBytes(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastToRoomAsync(string roomName, byte[] data)
|
||||||
|
{
|
||||||
|
if (!_rooms.TryGetValue(roomName, out var clients))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = clients.Where(id => _clients.ContainsKey(id))
|
||||||
|
.Select(id => SendDataAsync(_clients[id], data))
|
||||||
|
.ToArray();
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastToRoomExceptAsync(string roomName, byte[] data, string exceptClientId)
|
||||||
|
{
|
||||||
|
if (!_rooms.TryGetValue(roomName, out var clients))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = clients
|
||||||
|
.Where(id => _clients.ContainsKey(id) && id != exceptClientId)
|
||||||
|
.Select(id => SendDataAsync(_clients[id], data))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, Func<Connection, string, Task>> _commands = new();
|
||||||
|
|
||||||
|
public void RegisterCommand(string command, Func<Connection, string, Task> handler)
|
||||||
|
{
|
||||||
|
_commands[command] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCommand(Connection client, string commandLine)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(commandLine))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = commandLine.Split(' ');
|
||||||
|
var cmd = parts[0].ToUpperInvariant();
|
||||||
|
var args = parts.Length > 1 ? parts[1] : string.Empty;
|
||||||
|
|
||||||
|
if (_commands.TryGetValue(cmd, out var handler))
|
||||||
|
{
|
||||||
|
await handler(client, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task BroadcastToRoomAsync(string roomName, string message)
|
||||||
|
{
|
||||||
|
await BroadcastToRoomAsync(roomName, Encoding.UTF8.GetBytes(message));
|
||||||
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
_serverCancellation?.Cancel();
|
lock (_serverLock)
|
||||||
_tcpListener?.Stop();
|
{
|
||||||
_udpListener?.Close();
|
_serverCancellation?.Cancel();
|
||||||
|
_tcpListener?.Stop();
|
||||||
|
_udpListener?.Close();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var clientId in _clients.Keys.ToArray())
|
foreach (var clientId in _clients.Keys.ToArray())
|
||||||
{
|
{
|
||||||
|
@ -464,6 +776,14 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CleanupClientFromRooms(string clientId)
|
||||||
|
{
|
||||||
|
foreach (var room in _rooms.Keys.ToList())
|
||||||
|
{
|
||||||
|
LeaveRoom(clientId, room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose() => Stop();
|
public void Dispose() => Stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
39
README.md
39
README.md
|
@ -74,11 +74,11 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
|
||||||
{
|
{
|
||||||
Protocol = ProtocolType.TCP,
|
Protocol = ProtocolType.TCP,
|
||||||
Port = 1111,
|
Port = 1111,
|
||||||
UseSsl = false,
|
UseSsl = true,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
MaxConnections = 100000,
|
MaxConnections = 100000,
|
||||||
AesPassword = "EonaCat.Connections.Password",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss")
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss")
|
||||||
};
|
};
|
||||||
|
|
||||||
_server = new NetworkServer(config);
|
_server = new NetworkServer(config);
|
||||||
|
@ -131,9 +131,7 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
|
||||||
|
|
||||||
## Client example:
|
## Client example:
|
||||||
|
|
||||||
using EonaCat.Connections;
|
using EonaCat.Connections.Models;
|
||||||
using EonaCat.Connections.Models;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace EonaCat.Connections.Client.Example
|
namespace EonaCat.Connections.Client.Example
|
||||||
{
|
{
|
||||||
|
@ -150,8 +148,15 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
if (!_client.IsConnected)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Console.Write("Enter message to send (or 'exit' to quit): ");
|
Console.Write("Enter message to send (or 'exit' to quit): ");
|
||||||
var message = Console.ReadLine();
|
var message = Console.ReadLine();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await _client.DisconnectAsync().ConfigureAwait(false);
|
await _client.DisconnectAsync().ConfigureAwait(false);
|
||||||
|
@ -172,18 +177,27 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
|
||||||
Protocol = ProtocolType.TCP,
|
Protocol = ProtocolType.TCP,
|
||||||
Host = "127.0.0.1",
|
Host = "127.0.0.1",
|
||||||
Port = 1111,
|
Port = 1111,
|
||||||
UseSsl = false,
|
UseSsl = true,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
AesPassword = "EonaCat.Connections.Password",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
||||||
};
|
};
|
||||||
|
|
||||||
_client = new NetworkClient(config);
|
_client = new NetworkClient(config);
|
||||||
|
|
||||||
|
_client.OnGeneralError += (sender, e) =>
|
||||||
|
Console.WriteLine($"Error: {e.Message}");
|
||||||
|
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
_client.OnConnected += (sender, e) =>
|
_client.OnConnected += async (sender, e) =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Connected to server at {e.RemoteEndPoint}");
|
Console.WriteLine($"Connected to server at {e.RemoteEndPoint}");
|
||||||
|
|
||||||
|
// Set nickname
|
||||||
|
await _client.SetNicknameAsync("TestUser");
|
||||||
|
|
||||||
|
// Send a message
|
||||||
|
await _client.SendAsync("Hello server!");
|
||||||
};
|
};
|
||||||
|
|
||||||
_client.OnDataReceived += (sender, e) =>
|
_client.OnDataReceived += (sender, e) =>
|
||||||
|
@ -194,13 +208,8 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
|
||||||
Console.WriteLine("Disconnected from server");
|
Console.WriteLine("Disconnected from server");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Console.WriteLine("Connecting to server...");
|
||||||
await _client.ConnectAsync();
|
await _client.ConnectAsync();
|
||||||
|
|
||||||
// Send nickname
|
|
||||||
await _client.SendNicknameAsync("TestUser");
|
|
||||||
|
|
||||||
// Send a message
|
|
||||||
await _client.SendAsync("Hello server!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue