From bdf2d1b9350ababf9a84398572a18c5383f3fec7 Mon Sep 17 00:00:00 2001 From: Jeroen Saey Date: Thu, 21 Aug 2025 07:20:08 +0200 Subject: [PATCH] Updated --- EonaCat.Connections.Client/Program.cs | 39 ++- EonaCat.Connections.Server/Program.cs | 18 +- .../EonaCat.Connections.csproj | 8 - EonaCat.Connections/NetworkClient.cs | 255 ++++++++------- EonaCat.Connections/NetworkServer.cs | 300 ++++++++++-------- 5 files changed, 341 insertions(+), 279 deletions(-) diff --git a/EonaCat.Connections.Client/Program.cs b/EonaCat.Connections.Client/Program.cs index 1fbd48a..7dfdaf6 100644 --- a/EonaCat.Connections.Client/Program.cs +++ b/EonaCat.Connections.Client/Program.cs @@ -1,5 +1,6 @@ using EonaCat.Connections; using EonaCat.Connections.Models; +using System.Text; namespace EonaCat.Connections.Client.Example { @@ -7,12 +8,9 @@ namespace EonaCat.Connections.Client.Example { private static NetworkClient _client; - public static void Main(string[] args) + public static async Task Main(string[] args) { - for (int i = 0; i < 100000; i++) - { - CreateClientAsync(i).ConfigureAwait(false); - } + await CreateClientAsync().ConfigureAwait(false); while (true) { @@ -20,27 +18,42 @@ namespace EonaCat.Connections.Client.Example var message = Console.ReadLine(); if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase)) { - _client.DisconnectAsync().ConfigureAwait(false); + await _client.DisconnectAsync().ConfigureAwait(false); break; } + var jsonUrl = "https://microsoftedge.github.io/Demos/json-dummy-data/5MB-min.json"; + + try + { + using var httpClient = new HttpClient(); + var jsonContent = await httpClient.GetStringAsync(jsonUrl); + var jsonSize = Encoding.UTF8.GetByteCount(jsonContent); + Console.WriteLine($"Using large JSON file (size: {jsonSize / 1024 / 1024} MB)"); + message = jsonContent; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to download large JSON file: {ex.Message}"); + } + if (!string.IsNullOrEmpty(message)) { - _client.SendAsync(message).ConfigureAwait(false); + await _client.SendAsync(message).ConfigureAwait(false); } } } - private static async Task CreateClientAsync(int i) + private static async Task CreateClientAsync() { var config = new Configuration { Protocol = ProtocolType.TCP, Host = "127.0.0.1", - Port = 8080, - UseSsl = true, - UseAesEncryption = true, - ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"), + Port = 1111, + UseSsl = false, + UseAesEncryption = false, + //ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"), }; _client = new NetworkClient(config); @@ -58,7 +71,7 @@ namespace EonaCat.Connections.Client.Example await _client.ConnectAsync(); // Send nickname - await _client.SendNicknameAsync($"TestUser{i}"); + await _client.SendNicknameAsync("TestUser"); // Send a message await _client.SendAsync("Hello server!"); diff --git a/EonaCat.Connections.Server/Program.cs b/EonaCat.Connections.Server/Program.cs index f41ec5f..98c32be 100644 --- a/EonaCat.Connections.Server/Program.cs +++ b/EonaCat.Connections.Server/Program.cs @@ -35,26 +35,21 @@ namespace EonaCat.Connections.Server.Example var config = new Configuration { Protocol = ProtocolType.TCP, - Port = 8080, - UseSsl = true, - UseAesEncryption = true, + Port = 1111, + UseSsl = false, + UseAesEncryption = false, MaxConnections = 100000, - ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss"), + //ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss"), }; _server = new NetworkServer(config); - int totalClients = 0; + // Subscribe to events _server.OnConnected += (sender, e) => - { Console.WriteLine($"Client {e.ClientId} connected from {e.RemoteEndPoint}"); - Console.Title = $"Active Connections: {++totalClients}"; - }; _server.OnConnectedWithNickname += (sender, e) => - { Console.WriteLine($"Client {e.ClientId} connected with nickname: {e.Nickname}"); - }; _server.OnDataReceived += async (sender, e) => { @@ -72,10 +67,7 @@ namespace EonaCat.Connections.Server.Example }; _server.OnDisconnected += (sender, e) => - { Console.WriteLine($"Client {e.ClientId} disconnected"); - Console.Title = $"Active Connections: {--totalClients}"; - }; await _server.StartAsync(); } diff --git a/EonaCat.Connections/EonaCat.Connections.csproj b/EonaCat.Connections/EonaCat.Connections.csproj index 691545c..dacc565 100644 --- a/EonaCat.Connections/EonaCat.Connections.csproj +++ b/EonaCat.Connections/EonaCat.Connections.csproj @@ -11,10 +11,6 @@ EonaCat (Jeroen Saey) EonaCat.png readme.md - EonaCat.Connections - 1.0.1 - EonaCat (Jeroen Saey) - LICENSE @@ -22,10 +18,6 @@ True \ - - True - \ - True \ diff --git a/EonaCat.Connections/NetworkClient.cs b/EonaCat.Connections/NetworkClient.cs index b6d174e..2936634 100644 --- a/EonaCat.Connections/NetworkClient.cs +++ b/EonaCat.Connections/NetworkClient.cs @@ -143,39 +143,73 @@ namespace EonaCat.Connections } - private async Task ReceiveDataAsync() - { - var buffer = new byte[_config.BufferSize]; + private async Task ReceiveDataAsync() + { + while (!_cancellation.Token.IsCancellationRequested && _isConnected) + { + try + { + byte[] data; + + if (_config.UseAesEncryption && _aesEncryption != null) + { + // Read 4-byte length prefix + var lengthBuffer = new byte[4]; + int read = await ReadExactAsync(_stream, lengthBuffer, 4, _cancellation.Token); + if (read == 0) break; + + if (BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer); + int length = BitConverter.ToInt32(lengthBuffer, 0); + + // Read encrypted payload + var encrypted = new byte[length]; + await ReadExactAsync(_stream, encrypted, length, _cancellation.Token); + + // **Decrypt once here** + data = await DecryptDataAsync(encrypted, _aesEncryption); + } + else + { + data = new byte[_config.BufferSize]; + int bytesRead = await _stream.ReadAsync(data, 0, data.Length, _cancellation.Token); + if (bytesRead == 0) break; + + if (bytesRead < data.Length) + { + var tmp = new byte[bytesRead]; + Array.Copy(data, tmp, bytesRead); + data = tmp; + } + } + + await ProcessReceivedDataAsync(data); + } + catch (Exception ex) + { + OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" }); + _isConnected = false; + + _ = Task.Run(() => AutoReconnectAsync()); + break; + } + } + + await DisconnectAsync(); + } + + + private async Task ReadExactAsync(Stream stream, byte[] buffer, int length, CancellationToken ct) + { + int offset = 0; + while (offset < length) + { + int read = await stream.ReadAsync(buffer, offset, length - offset, ct); + if (read == 0) return 0; + offset += read; + } + return offset; + } - while (!_cancellation.Token.IsCancellationRequested && _isConnected) - { - try - { - var bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cancellation.Token); - - if (bytesRead == 0) - { - break; - } - - var data = new byte[bytesRead]; - Array.Copy(buffer, data, bytesRead); - - await ProcessReceivedDataAsync(data); - } - catch (Exception ex) - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" }); - _isConnected = false; - - // Start reconnect - _ = Task.Run(() => AutoReconnectAsync()); - break; - } - } - - await DisconnectAsync(); - } private async Task ReceiveUdpDataAsync() { @@ -198,91 +232,86 @@ namespace EonaCat.Connections } } - private async Task ProcessReceivedDataAsync(byte[] data) - { - try - { - // Decrypt if AES encryption is enabled - if (_config.UseAesEncryption && _aesEncryption != null) - { - data = await DecryptDataAsync(data, _aesEncryption); - } + private async Task ProcessReceivedDataAsync(byte[] data) + { + try + { + // Data is already decrypted if AES is enabled + // Just update stats / handle string conversion + + bool isBinary = true; + string stringData = null; + + try + { + stringData = Encoding.UTF8.GetString(data); + if (Encoding.UTF8.GetBytes(stringData).Length == data.Length) + isBinary = false; + } + catch + { + // Keep as binary + } + + OnDataReceived?.Invoke(this, new DataReceivedEventArgs + { + ClientId = "server", + Data = data, + StringData = stringData, + IsBinary = isBinary + }); + } + catch (Exception ex) + { + if (_config.UseAesEncryption) + OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" }); + else + OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" }); + } + } - // Try to decode as string, fallback to binary - bool isBinary = true; - string stringData = null; - try - { - stringData = Encoding.UTF8.GetString(data); - if (Encoding.UTF8.GetBytes(stringData).Length == data.Length) - { - isBinary = false; - } - } - catch - { - // Keep as binary - } - - OnDataReceived?.Invoke(this, new DataReceivedEventArgs - { - ClientId = "server", - Data = data, - StringData = stringData, - IsBinary = isBinary - }); - } - catch (Exception ex) - { - if (_config.UseAesEncryption) - { - OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error decrypting data" }); - } - else - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing received data" }); - } - } + public async Task SendAsync(byte[] data) + { + if (!_isConnected) return; + + try + { + if (_config.UseAesEncryption && _aesEncryption != null) + { + // Encrypt payload + data = await EncryptDataAsync(data, _aesEncryption); + + // Prepend 4-byte length for framing + var lengthPrefix = BitConverter.GetBytes(data.Length); + if (BitConverter.IsLittleEndian) Array.Reverse(lengthPrefix); + + var framed = new byte[lengthPrefix.Length + data.Length]; + Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length); + Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length); + + data = framed; + } + + if (_config.Protocol == ProtocolType.TCP) + { + await _stream.WriteAsync(data, 0, data.Length); + await _stream.FlushAsync(); + } + else + { + await _udpClient.SendAsync(data, data.Length); + } + } + catch (Exception ex) + { + if (_config.UseAesEncryption) + OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error encrypting/sending data" }); + else + OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" }); + } } - public async Task SendAsync(byte[] data) - { - if (!_isConnected) - { - return; - } - - try - { - // Encrypt if AES encryption is enabled - if (_config.UseAesEncryption && _aesEncryption != null) - { - data = await EncryptDataAsync(data, _aesEncryption); - } - - if (_config.Protocol == ProtocolType.TCP) - { - await _stream.WriteAsync(data, 0, data.Length); - await _stream.FlushAsync(); - } - else - { - await _udpClient.SendAsync(data, data.Length); - } - } - catch (Exception ex) - { - if (_config.UseAesEncryption) - { - OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error encrypting/sending data" }); - } - else - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" }); - } - } - } public async Task SendAsync(string message) { diff --git a/EonaCat.Connections/NetworkServer.cs b/EonaCat.Connections/NetworkServer.cs index 82450be..3350539 100644 --- a/EonaCat.Connections/NetworkServer.cs +++ b/EonaCat.Connections/NetworkServer.cs @@ -243,103 +243,131 @@ namespace EonaCat.Connections await ProcessReceivedDataAsync(client, result.Buffer); } - private async Task HandleClientCommunicationAsync(Connection client) - { - var buffer = new byte[_config.BufferSize]; + private async Task HandleClientCommunicationAsync(Connection client) + { + var lengthBuffer = new byte[4]; // length prefix + + while (!client.CancellationToken.Token.IsCancellationRequested && client.TcpClient.Connected) + { + try + { + byte[] data; + + if (client.IsEncrypted && client.AesEncryption != null) + { + // Read 4-byte length first + int read = await ReadExactAsync(client.Stream, lengthBuffer, 4, client.CancellationToken.Token); + if (read == 0) break; + + if (BitConverter.IsLittleEndian) + Array.Reverse(lengthBuffer); + + int length = BitConverter.ToInt32(lengthBuffer, 0); + + // Read full encrypted message + var encrypted = new byte[length]; + await ReadExactAsync(client.Stream, encrypted, length, client.CancellationToken.Token); + + // **Decrypt once here** + data = await DecryptDataAsync(encrypted, client.AesEncryption); + } + else + { + // Non-encrypted: just read raw bytes + data = new byte[_config.BufferSize]; + int bytesRead = await client.Stream.ReadAsync(data, 0, data.Length, client.CancellationToken.Token); + if (bytesRead == 0) break; + + if (bytesRead < data.Length) + { + var tmp = new byte[bytesRead]; + Array.Copy(data, tmp, bytesRead); + data = tmp; + } + } + + await ProcessReceivedDataAsync(client, data); + } + catch (Exception ex) + { + OnGeneralError?.Invoke(this, new ErrorEventArgs + { + ClientId = client.Id, + Exception = ex, + Message = "Error reading from client" + }); + break; + } + } + } + + private async Task ReadExactAsync(Stream stream, byte[] buffer, int length, CancellationToken ct) + { + int offset = 0; + while (offset < length) + { + int read = await stream.ReadAsync(buffer, offset, length - offset, ct); + if (read == 0) return 0; // disconnected + offset += read; + } + return offset; + } - while (!client.CancellationToken.Token.IsCancellationRequested && client.TcpClient.Connected) - { - try - { - var bytesRead = await client.Stream.ReadAsync(buffer, 0, buffer.Length, client.CancellationToken.Token); - if (bytesRead == 0) - { - break; // Client disconnected - } + private async Task ProcessReceivedDataAsync(Connection client, byte[] data) + { + try + { + client.BytesReceived += data.Length; + lock (_statsLock) + { + _stats.BytesReceived += data.Length; + _stats.MessagesReceived++; + } + + // Try to decode as string, fallback to binary + bool isBinary = true; + string stringData = null; + + try + { + stringData = Encoding.UTF8.GetString(data); + if (Encoding.UTF8.GetBytes(stringData).Length == data.Length) + isBinary = false; + } + catch { } + + // Handle special commands + if (!isBinary && stringData.StartsWith("NICKNAME:")) + { + var nickname = stringData.Substring(9); + client.Nickname = nickname; + OnConnectedWithNickname?.Invoke(this, new NicknameConnectionEventArgs + { + ClientId = client.Id, + RemoteEndPoint = client.RemoteEndPoint, + Nickname = nickname + }); + return; + } + + OnDataReceived?.Invoke(this, new DataReceivedEventArgs + { + ClientId = client.Id, + Data = data, + StringData = stringData, + IsBinary = isBinary + }); + } + catch (Exception ex) + { + if (client.IsEncrypted) + OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" }); + else + OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" }); + } + } - var data = new byte[bytesRead]; - Array.Copy(buffer, data, bytesRead); - - await ProcessReceivedDataAsync(client, data); - } - catch (Exception ex) - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error reading from client" }); - break; - } - } - } - - private async Task ProcessReceivedDataAsync(Connection client, byte[] data) - { - try - { - // Decrypt if AES encryption is enabled - if (client.IsEncrypted && client.AesEncryption != null) - { - data = await DecryptDataAsync(data, client.AesEncryption); - } - - client.BytesReceived += data.Length; - lock (_statsLock) - { - _stats.BytesReceived += data.Length; - _stats.MessagesReceived++; - } - - // Try to decode as string, fallback to binary - bool isBinary = true; - string stringData = null; - - try - { - stringData = Encoding.UTF8.GetString(data); - // Check if it's valid UTF-8 - if (Encoding.UTF8.GetBytes(stringData).Length == data.Length) - { - isBinary = false; - } - } - catch - { - // Keep as binary - } - - // Handle special commands - if (!isBinary && stringData.StartsWith("NICKNAME:")) - { - var nickname = stringData.Substring(9); - client.Nickname = nickname; - OnConnectedWithNickname?.Invoke(this, new NicknameConnectionEventArgs - { - ClientId = client.Id, - RemoteEndPoint = client.RemoteEndPoint, - Nickname = nickname - }); - return; - } - - OnDataReceived?.Invoke(this, new DataReceivedEventArgs - { - ClientId = client.Id, - Data = data, - StringData = stringData, - IsBinary = isBinary - }); - } - catch (Exception ex) - { - if (client.IsEncrypted) - { - OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error decrypting data" }); - } - else - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing received data" }); - } - } - } public async Task SendToClientAsync(string clientId, byte[] data) { @@ -369,46 +397,54 @@ namespace EonaCat.Connections await BroadcastAsync(Encoding.UTF8.GetBytes(message)); } - private async Task SendDataAsync(Connection client, byte[] data) - { - try - { - // Encrypt if AES encryption is enabled - if (client.IsEncrypted && client.AesEncryption != null) - { - data = await EncryptDataAsync(data, client.AesEncryption); - } - - if (_config.Protocol == ProtocolType.TCP) - { - await client.Stream.WriteAsync(data, 0, data.Length); - await client.Stream.FlushAsync(); - } - else - { - await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint); - } - - client.BytesSent += data.Length; - lock (_statsLock) - { - _stats.BytesSent += data.Length; - _stats.MessagesSent++; - } - } - catch (Exception ex) - { - if (client.IsEncrypted) - { - OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" }); - } - else - { - OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" }); - } - } + private async Task SendDataAsync(Connection client, byte[] data) + { + try + { + if (client.IsEncrypted && client.AesEncryption != null) + { + // Encrypt payload + data = await EncryptDataAsync(data, client.AesEncryption); + + // Prepend length for safe framing + var lengthPrefix = BitConverter.GetBytes(data.Length); + if (BitConverter.IsLittleEndian) + Array.Reverse(lengthPrefix); + + var framed = new byte[lengthPrefix.Length + data.Length]; + Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length); + Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length); + + data = framed; // replace data with framed payload + } + + if (_config.Protocol == ProtocolType.TCP) + { + await client.Stream.WriteAsync(data, 0, data.Length); + await client.Stream.FlushAsync(); + } + else + { + await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint); + } + + client.BytesSent += data.Length; + lock (_statsLock) + { + _stats.BytesSent += data.Length; + _stats.MessagesSent++; + } + } + catch (Exception ex) + { + if (client.IsEncrypted) + OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" }); + else + OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" }); + } } + private async Task EncryptDataAsync(byte[] data, Aes aes) { using (var encryptor = aes.CreateEncryptor())