Added rooms
This commit is contained in:
		| @@ -11,4 +11,10 @@ | ||||
|     <ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Update="client.pfx"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </None> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -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!"); | ||||
|   | ||||
| @@ -11,4 +11,10 @@ | ||||
|     <ProjectReference Include="..\EonaCat.Connections\EonaCat.Connections.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Update="server.pfx"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </None> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; } | ||||
|     } | ||||
| } | ||||
| @@ -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<string> _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<ConnectionEventArgs> 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(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <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 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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<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> 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<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; | ||||
|                 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<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) | ||||
| @@ -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<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) | ||||
|                 { | ||||
|                     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() | ||||
|         { | ||||
|             _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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										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, | ||||
|                     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!"); | ||||
|             } | ||||
|         } | ||||
| } | ||||
|     } | ||||
		Reference in New Issue
	
	Block a user