diff --git a/EonaCat.Connections.Client/EonaCat.Connections.Client.csproj b/EonaCat.Connections.Client/EonaCat.Connections.Client.csproj
index f01369b..c448043 100644
--- a/EonaCat.Connections.Client/EonaCat.Connections.Client.csproj
+++ b/EonaCat.Connections.Client/EonaCat.Connections.Client.csproj
@@ -11,4 +11,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/EonaCat.Connections.Client/Program.cs b/EonaCat.Connections.Client/Program.cs
index 5b79ec3..6f6100b 100644
--- a/EonaCat.Connections.Client/Program.cs
+++ b/EonaCat.Connections.Client/Program.cs
@@ -44,10 +44,10 @@ namespace EonaCat.Connections.Client.Example
Protocol = ProtocolType.TCP,
Host = "127.0.0.1",
Port = 1111,
- UseSsl = false,
+ UseSsl = true,
UseAesEncryption = true,
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);
@@ -60,8 +60,8 @@ namespace EonaCat.Connections.Client.Example
{
Console.WriteLine($"Connected to server at {e.RemoteEndPoint}");
- // Send nickname
- await _client.SendNicknameAsync("TestUser");
+ // Set nickname
+ await _client.SetNicknameAsync("TestUser");
// Send a message
await _client.SendAsync("Hello server!");
diff --git a/EonaCat.Connections.Server/EonaCat.Connections.Server.csproj b/EonaCat.Connections.Server/EonaCat.Connections.Server.csproj
index f01369b..9dfcd2b 100644
--- a/EonaCat.Connections.Server/EonaCat.Connections.Server.csproj
+++ b/EonaCat.Connections.Server/EonaCat.Connections.Server.csproj
@@ -11,4 +11,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/EonaCat.Connections.Server/Program.cs b/EonaCat.Connections.Server/Program.cs
index 57efa1c..f2a87b2 100644
--- a/EonaCat.Connections.Server/Program.cs
+++ b/EonaCat.Connections.Server/Program.cs
@@ -38,11 +38,11 @@ namespace EonaCat.Connections.Server.Example
{
Protocol = ProtocolType.TCP,
Port = 1111,
- UseSsl = false,
+ UseSsl = true,
UseAesEncryption = true,
MaxConnections = 100000,
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);
diff --git a/EonaCat.Connections/EventArguments/ErrorEventArgs.cs b/EonaCat.Connections/EventArguments/ErrorEventArgs.cs
index ad86f07..9afc4b6 100644
--- a/EonaCat.Connections/EventArguments/ErrorEventArgs.cs
+++ b/EonaCat.Connections/EventArguments/ErrorEventArgs.cs
@@ -6,6 +6,7 @@
public class ErrorEventArgs : EventArgs
{
public string ClientId { get; set; }
+ public string Nickname { get; set; }
public Exception Exception { get; set; }
public string Message { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
diff --git a/EonaCat.Connections/Models/Connection.cs b/EonaCat.Connections/Models/Connection.cs
index 51f1625..1e8fdd6 100644
--- a/EonaCat.Connections/Models/Connection.cs
+++ b/EonaCat.Connections/Models/Connection.cs
@@ -48,5 +48,6 @@ namespace EonaCat.Connections.Models
public CancellationTokenSource CancellationToken { get; set; }
public long BytesSent { get; set; }
public long BytesReceived { get; set; }
+ public SemaphoreSlim SendLock { get; internal set; }
}
}
\ No newline at end of file
diff --git a/EonaCat.Connections/NetworkClient.cs b/EonaCat.Connections/NetworkClient.cs
index d2cebd6..afb8480 100644
--- a/EonaCat.Connections/NetworkClient.cs
+++ b/EonaCat.Connections/NetworkClient.cs
@@ -11,9 +11,6 @@ using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
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
{
private readonly Configuration _config;
@@ -24,7 +21,25 @@ namespace EonaCat.Connections
private CancellationTokenSource _cancellation;
private bool _isConnected;
- public bool IsConnected => _isConnected;
+ private readonly object _stateLock = new object();
+ private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
+
+ private readonly HashSet _joinedRooms = new();
+
+ public bool IsConnected
+ {
+ get { lock (_stateLock)
+ {
+ return _isConnected;
+ }
+ }
+ private set { lock (_stateLock)
+ {
+ _isConnected = value;
+ }
+ }
+ }
+
public bool IsAutoReconnecting { get; private set; }
public event EventHandler OnConnected;
@@ -41,8 +56,11 @@ namespace EonaCat.Connections
public async Task ConnectAsync()
{
- _cancellation?.Cancel();
- _cancellation = new CancellationTokenSource();
+ lock (_stateLock)
+ {
+ _cancellation?.Cancel();
+ _cancellation = new CancellationTokenSource();
+ }
if (_config.Protocol == ProtocolType.TCP)
{
@@ -58,10 +76,10 @@ namespace EonaCat.Connections
{
try
{
- _tcpClient = new TcpClient();
- await _tcpClient.ConnectAsync(_config.Host, _config.Port);
+ var client = new TcpClient();
+ await client.ConnectAsync(_config.Host, _config.Port);
- Stream stream = _tcpClient.GetStream();
+ Stream stream = client.GetStream();
if (_config.UseSsl)
{
@@ -99,8 +117,12 @@ namespace EonaCat.Connections
}
}
- _stream = stream;
- _isConnected = true;
+ lock (_stateLock)
+ {
+ _tcpClient = client;
+ _stream = stream;
+ IsConnected = true;
+ }
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)
{
- _isConnected = false;
+ IsConnected = false;
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect" });
_ = Task.Run(() => AutoReconnectAsync());
}
@@ -118,9 +140,14 @@ namespace EonaCat.Connections
{
try
{
- _udpClient = new UdpClient();
- _udpClient.Connect(_config.Host, _config.Port);
- _isConnected = true;
+ var client = new UdpClient();
+ client.Connect(_config.Host, _config.Port);
+
+ lock (_stateLock)
+ {
+ _udpClient = client;
+ IsConnected = true;
+ }
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)
{
- _isConnected = false;
+ IsConnected = false;
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect UDP" });
}
}
private async Task ReceiveDataAsync(CancellationToken ct)
{
- while (!ct.IsCancellationRequested && _isConnected)
+ while (!ct.IsCancellationRequested && IsConnected)
{
try
{
@@ -182,7 +209,7 @@ namespace EonaCat.Connections
}
catch (Exception ex)
{
- _isConnected = false;
+ IsConnected = false;
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
_ = Task.Run(() => AutoReconnectAsync());
break;
@@ -210,7 +237,7 @@ namespace EonaCat.Connections
private async Task ReceiveUdpDataAsync(CancellationToken ct)
{
- while (!ct.IsCancellationRequested && _isConnected)
+ while (!ct.IsCancellationRequested && IsConnected)
{
try
{
@@ -220,7 +247,7 @@ namespace EonaCat.Connections
catch (Exception ex)
{
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
- _isConnected = false;
+ IsConnected = false;
_ = Task.Run(() => AutoReconnectAsync());
break;
}
@@ -258,11 +285,12 @@ namespace EonaCat.Connections
public async Task SendAsync(byte[] data)
{
- if (!_isConnected)
+ if (!IsConnected)
{
return;
}
+ await _sendLock.WaitAsync();
try
{
if (_config.UseAesEncryption && _aesEncryption != null)
@@ -296,10 +324,52 @@ namespace EonaCat.Connections
var handler = _config.UseAesEncryption ? OnEncryptionError : OnGeneralError;
handler?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
}
+ finally
+ {
+ _sendLock.Release();
+ }
+ }
+
+ /// Join a room (server should recognize this command)
+ 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 GetJoinedRooms()
+ {
+ return _joinedRooms.ToList().AsReadOnly();
}
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()
{
@@ -309,18 +379,17 @@ namespace EonaCat.Connections
}
int attempt = 0;
+ IsAutoReconnecting = true;
- while (!_isConnected && (_config.MaxReconnectAttempts == 0 || attempt < _config.MaxReconnectAttempts))
+ while (!IsConnected && (_config.MaxReconnectAttempts == 0 || attempt < _config.MaxReconnectAttempts))
{
attempt++;
try
{
- IsAutoReconnecting = true;
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnecting attempt {attempt}" });
await ConnectAsync();
- if (_isConnected)
+ if (IsConnected)
{
- IsAutoReconnecting = false;
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnected after {attempt} attempt(s)" });
break;
}
@@ -330,22 +399,43 @@ namespace EonaCat.Connections
await Task.Delay(_config.ReconnectDelayMs);
}
- if (!_isConnected)
+ if (!IsConnected)
{
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()
{
- _isConnected = false;
- _cancellation?.Cancel();
+ lock (_stateLock)
+ {
+ if (!IsConnected)
+ {
+ return;
+ }
+
+ IsConnected = false;
+ _cancellation?.Cancel();
+ }
_tcpClient?.Close();
_udpClient?.Close();
_stream?.Dispose();
_aesEncryption?.Dispose();
-
+ _joinedRooms?.Clear();
+
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self" });
}
@@ -354,6 +444,7 @@ namespace EonaCat.Connections
_cancellation?.Cancel();
DisconnectAsync().Wait();
_cancellation?.Dispose();
+ _sendLock.Dispose();
}
}
}
diff --git a/EonaCat.Connections/NetworkServer.cs b/EonaCat.Connections/NetworkServer.cs
index 15d7db4..b104676 100644
--- a/EonaCat.Connections/NetworkServer.cs
+++ b/EonaCat.Connections/NetworkServer.cs
@@ -12,9 +12,6 @@ using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
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
{
private readonly Configuration _config;
@@ -24,6 +21,13 @@ namespace EonaCat.Connections
private UdpClient _udpListener;
private CancellationTokenSource _serverCancellation;
private readonly object _statsLock = new object();
+ private readonly object _serverLock = new object();
+
+ private readonly ConcurrentDictionary> _rooms = new();
+ private readonly ConcurrentDictionary> _roomHistory = new();
+ private readonly ConcurrentDictionary _roomPasswords = new();
+ private readonly ConcurrentDictionary _rateLimits = new();
+ private readonly int _maxMessagesPerSecond = 10;
public event EventHandler OnConnected;
public event EventHandler OnConnectedWithNickname;
@@ -54,23 +58,47 @@ namespace EonaCat.Connections
public async Task StartAsync()
{
- _serverCancellation?.Cancel();
- _serverCancellation = new CancellationTokenSource();
+ lock (_serverLock)
+ {
+ if (_serverCancellation != null && !_serverCancellation.IsCancellationRequested)
+ {
+ // Server is already running
+ return;
+ }
- if (_config.Protocol == ProtocolType.TCP)
- {
- await StartTcpServerAsync();
+ _serverCancellation = new CancellationTokenSource();
}
- 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()
{
- _tcpListener = new TcpListener(IPAddress.Parse(_config.Host), _config.Port);
- _tcpListener.Start();
+ lock (_serverLock)
+ {
+ 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}");
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()
{
- _udpListener = new UdpClient(_config.Port);
+ lock (_serverLock)
+ {
+ _udpListener?.Close();
+ _udpListener = new UdpClient(_config.Port);
+ }
+
Console.WriteLine($"UDP Server started on {_config.Host}:{_config.Port}");
+ _ = Task.Run(() => CleanupInactiveUdpClientsAsync(), _serverCancellation.Token);
while (!_serverCancellation.Token.IsCancellationRequested)
{
@@ -100,7 +172,10 @@ namespace EonaCat.Connections
var result = await _udpListener.ReceiveAsync();
_ = Task.Run(() => HandleUdpDataAsync(result), _serverCancellation.Token);
}
- catch (ObjectDisposedException) { break; }
+ catch (ObjectDisposedException)
+ {
+ break;
+ }
catch (Exception ex)
{
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)
{
var clientId = Guid.NewGuid().ToString();
@@ -118,7 +194,8 @@ namespace EonaCat.Connections
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint,
ConnectedAt = DateTime.UtcNow,
LastActive = DateTime.UtcNow,
- CancellationToken = new CancellationTokenSource()
+ CancellationToken = new CancellationTokenSource(),
+ SendLock = new SemaphoreSlim(1, 1)
};
try
@@ -147,7 +224,8 @@ namespace EonaCat.Connections
}
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;
}
}
@@ -156,7 +234,6 @@ namespace EonaCat.Connections
{
try
{
- // Create AES object
client.AesEncryption = Aes.Create();
client.AesEncryption.KeySize = 256;
client.AesEncryption.BlockSize = 128;
@@ -164,17 +241,12 @@ namespace EonaCat.Connections
client.AesEncryption.Padding = PaddingMode.PKCS7;
client.IsEncrypted = true;
- // Send salt to client to derive key
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
}
catch (Exception ex)
{
- OnEncryptionError?.Invoke(this, new ErrorEventArgs
- {
- ClientId = clientId,
- Exception = ex,
- Message = "AES setup failed"
- });
+ var handler = OnEncryptionError;
+ handler?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "AES setup failed" });
return;
}
}
@@ -184,13 +256,15 @@ namespace EonaCat.Connections
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);
}
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
{
@@ -198,8 +272,6 @@ namespace EonaCat.Connections
}
}
-
-
private async Task HandleUdpDataAsync(UdpReceiveResult result)
{
var clientKey = result.RemoteEndPoint.ToString();
@@ -209,11 +281,15 @@ namespace EonaCat.Connections
{
Id = clientKey,
RemoteEndPoint = result.RemoteEndPoint,
- ConnectedAt = DateTime.UtcNow
+ ConnectedAt = DateTime.UtcNow,
+ SendLock = new SemaphoreSlim(1, 1)
};
_clients[clientKey] = client;
+
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);
@@ -269,7 +345,8 @@ namespace EonaCat.Connections
}
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;
}
}
@@ -295,6 +372,13 @@ namespace EonaCat.Connections
{
try
{
+ if (!CheckRateLimit(client.Id))
+ {
+ // Throttle the client
+ await Task.Delay(100);
+ return;
+ }
+
client.BytesReceived += data.Length;
lock (_statsLock)
{
@@ -316,7 +400,13 @@ namespace EonaCat.Connections
if (stringData.StartsWith("NICKNAME:"))
{
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;
}
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
@@ -324,10 +414,61 @@ namespace EonaCat.Connections
DisconnectClient(client.Id);
return;
}
+ else if (stringData.StartsWith("JOIN_ROOM:"))
+ {
+ string roomName = stringData.Substring(10);
+ var bag = _rooms.GetOrAdd(roomName, _ => new ConcurrentBag());
+ 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(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());
+ history.Enqueue($"{client.Nickname}:{msg}");
+ while (history.Count > 100)
+ {
+ history.TryDequeue(out _);
+ }
+ }
+ }
+ return;
+ }
+ else
+ {
+ await HandleCommand(client, stringData);
+ }
}
client.LastActive = DateTime.UtcNow;
- OnDataReceived?.Invoke(this, new DataReceivedEventArgs
+ var dataHandler = OnDataReceived;
+ dataHandler?.Invoke(this, new DataReceivedEventArgs
{
ClientId = client.Id,
Nickname = client.Nickname,
@@ -340,20 +481,18 @@ namespace EonaCat.Connections
catch (Exception ex)
{
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)
{
+ await client.SendLock.WaitAsync();
try
{
- // Encrypt if AES is enabled
if (client.IsEncrypted && client.AesEncryption != null)
{
data = await AesCryptoHelpers.EncryptDataAsync(data, client.AesEncryption);
-
- // Prepend 4-byte length (big-endian) for framing
var lengthPrefix = BitConverter.GetBytes(data.Length);
if (BitConverter.IsLittleEndian)
{
@@ -377,7 +516,6 @@ namespace EonaCat.Connections
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
}
- // Update stats
client.BytesSent += data.Length;
lock (_statsLock)
{
@@ -388,13 +526,86 @@ namespace EonaCat.Connections
catch (Exception ex)
{
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
- handler?.Invoke(this, new ErrorEventArgs
- {
- ClientId = client.Id,
- Exception = ex,
- Message = "Error sending data"
- });
+ handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data", Nickname = client.Nickname });
}
+ 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());
+ 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 GetRoomHistory(string roomName)
+ {
+ if (_roomHistory.TryGetValue(roomName, out var queue))
+ {
+ return queue.ToArray();
+ }
+
+ return Enumerable.Empty();
+ }
+
+ 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 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)
@@ -405,7 +616,6 @@ namespace EonaCat.Connections
return;
}
- // Fallback: try nickname
foreach (var kvp in _clients)
{
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
@@ -438,25 +648,127 @@ namespace EonaCat.Connections
{
try
{
+ CleanupClientFromRooms(clientId);
+
client.CancellationToken?.Cancel();
client.TcpClient?.Close();
client.Stream?.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(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)
{
- 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());
+ bag.Add(clientId);
+ }
+
+ public void LeaveRoom(string clientId, string roomName)
+ {
+ if (_rooms.TryGetValue(roomName, out var bag))
+ {
+ var newBag = new ConcurrentBag(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> _commands = new();
+
+ public void RegisterCommand(string command, Func 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()
{
- _serverCancellation?.Cancel();
- _tcpListener?.Stop();
- _udpListener?.Close();
+ lock (_serverLock)
+ {
+ _serverCancellation?.Cancel();
+ _tcpListener?.Stop();
+ _udpListener?.Close();
+ }
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();
}
}
diff --git a/README.md b/README.md
index d2ad8a9..ce68427 100644
--- a/README.md
+++ b/README.md
@@ -74,11 +74,11 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
{
Protocol = ProtocolType.TCP,
Port = 1111,
- UseSsl = false,
+ UseSsl = true,
UseAesEncryption = true,
MaxConnections = 100000,
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);
@@ -131,9 +131,7 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
## Client example:
- using EonaCat.Connections;
- using EonaCat.Connections.Models;
- using System.Text;
+ using EonaCat.Connections.Models;
namespace EonaCat.Connections.Client.Example
{
@@ -150,8 +148,15 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
while (true)
{
+ if (!_client.IsConnected)
+ {
+ await Task.Delay(1000).ConfigureAwait(false);
+ continue;
+ }
+
Console.Write("Enter message to send (or 'exit' to quit): ");
var message = Console.ReadLine();
+
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
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,
Host = "127.0.0.1",
Port = 1111,
- UseSsl = false,
+ UseSsl = true,
UseAesEncryption = true,
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.OnGeneralError += (sender, e) =>
+ Console.WriteLine($"Error: {e.Message}");
+
// Subscribe to events
- _client.OnConnected += (sender, e) =>
+ _client.OnConnected += async (sender, e) =>
{
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) =>
@@ -194,13 +208,8 @@ servers and clients with optional TLS (for TCP) and optional application-layer e
Console.WriteLine("Disconnected from server");
};
+ Console.WriteLine("Connecting to server...");
await _client.ConnectAsync();
-
- // Send nickname
- await _client.SendNicknameAsync("TestUser");
-
- // Send a message
- await _client.SendAsync("Hello server!");
}
}
-}
\ No newline at end of file
+ }
\ No newline at end of file