This commit is contained in:
EonaCat 2025-08-21 07:20:08 +02:00 committed by Jeroen Saey
parent 1a15416613
commit bdf2d1b935
5 changed files with 341 additions and 279 deletions

View File

@ -1,5 +1,6 @@
using EonaCat.Connections; using EonaCat.Connections;
using EonaCat.Connections.Models; using EonaCat.Connections.Models;
using System.Text;
namespace EonaCat.Connections.Client.Example namespace EonaCat.Connections.Client.Example
{ {
@ -7,12 +8,9 @@ namespace EonaCat.Connections.Client.Example
{ {
private static NetworkClient _client; private static NetworkClient _client;
public static void Main(string[] args) public static async Task Main(string[] args)
{ {
for (int i = 0; i < 100000; i++) await CreateClientAsync().ConfigureAwait(false);
{
CreateClientAsync(i).ConfigureAwait(false);
}
while (true) while (true)
{ {
@ -20,27 +18,42 @@ namespace EonaCat.Connections.Client.Example
var message = Console.ReadLine(); var message = Console.ReadLine();
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
{ {
_client.DisconnectAsync().ConfigureAwait(false); await _client.DisconnectAsync().ConfigureAwait(false);
break; 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)) 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 var config = new Configuration
{ {
Protocol = ProtocolType.TCP, Protocol = ProtocolType.TCP,
Host = "127.0.0.1", Host = "127.0.0.1",
Port = 8080, Port = 1111,
UseSsl = true, UseSsl = false,
UseAesEncryption = true, UseAesEncryption = false,
ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"), //ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
}; };
_client = new NetworkClient(config); _client = new NetworkClient(config);
@ -58,7 +71,7 @@ namespace EonaCat.Connections.Client.Example
await _client.ConnectAsync(); await _client.ConnectAsync();
// Send nickname // Send nickname
await _client.SendNicknameAsync($"TestUser{i}"); await _client.SendNicknameAsync("TestUser");
// Send a message // Send a message
await _client.SendAsync("Hello server!"); await _client.SendAsync("Hello server!");

View File

@ -35,26 +35,21 @@ namespace EonaCat.Connections.Server.Example
var config = new Configuration var config = new Configuration
{ {
Protocol = ProtocolType.TCP, Protocol = ProtocolType.TCP,
Port = 8080, Port = 1111,
UseSsl = true, UseSsl = false,
UseAesEncryption = true, UseAesEncryption = false,
MaxConnections = 100000, 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); _server = new NetworkServer(config);
int totalClients = 0;
// Subscribe to events // Subscribe to events
_server.OnConnected += (sender, e) => _server.OnConnected += (sender, e) =>
{
Console.WriteLine($"Client {e.ClientId} connected from {e.RemoteEndPoint}"); Console.WriteLine($"Client {e.ClientId} connected from {e.RemoteEndPoint}");
Console.Title = $"Active Connections: {++totalClients}";
};
_server.OnConnectedWithNickname += (sender, e) => _server.OnConnectedWithNickname += (sender, e) =>
{
Console.WriteLine($"Client {e.ClientId} connected with nickname: {e.Nickname}"); Console.WriteLine($"Client {e.ClientId} connected with nickname: {e.Nickname}");
};
_server.OnDataReceived += async (sender, e) => _server.OnDataReceived += async (sender, e) =>
{ {
@ -72,10 +67,7 @@ namespace EonaCat.Connections.Server.Example
}; };
_server.OnDisconnected += (sender, e) => _server.OnDisconnected += (sender, e) =>
{
Console.WriteLine($"Client {e.ClientId} disconnected"); Console.WriteLine($"Client {e.ClientId} disconnected");
Console.Title = $"Active Connections: {--totalClients}";
};
await _server.StartAsync(); await _server.StartAsync();
} }

View File

@ -11,10 +11,6 @@
<Copyright>EonaCat (Jeroen Saey)</Copyright> <Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageIcon>EonaCat.png</PackageIcon> <PackageIcon>EonaCat.png</PackageIcon>
<PackageReadmeFile>readme.md</PackageReadmeFile> <PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageId>EonaCat.Connections</PackageId>
<Version>1.0.1</Version>
<Authors>EonaCat (Jeroen Saey)</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -22,10 +18,6 @@
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
<None Include="..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\readme.md"> <None Include="..\readme.md">
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>

View File

@ -143,39 +143,73 @@ namespace EonaCat.Connections
} }
private async Task ReceiveDataAsync() private async Task ReceiveDataAsync()
{ {
var buffer = new byte[_config.BufferSize]; 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<int> 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() private async Task ReceiveUdpDataAsync()
{ {
@ -198,91 +232,86 @@ namespace EonaCat.Connections
} }
} }
private async Task ProcessReceivedDataAsync(byte[] data) private async Task ProcessReceivedDataAsync(byte[] data)
{ {
try try
{ {
// Decrypt if AES encryption is enabled // Data is already decrypted if AES is enabled
if (_config.UseAesEncryption && _aesEncryption != null) // Just update stats / handle string conversion
{
data = await DecryptDataAsync(data, _aesEncryption); 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 public async Task SendAsync(byte[] data)
{ {
stringData = Encoding.UTF8.GetString(data); if (!_isConnected) return;
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
{ try
isBinary = false; {
} if (_config.UseAesEncryption && _aesEncryption != null)
} {
catch // Encrypt payload
{ data = await EncryptDataAsync(data, _aesEncryption);
// Keep as binary
} // Prepend 4-byte length for framing
var lengthPrefix = BitConverter.GetBytes(data.Length);
OnDataReceived?.Invoke(this, new DataReceivedEventArgs if (BitConverter.IsLittleEndian) Array.Reverse(lengthPrefix);
{
ClientId = "server", var framed = new byte[lengthPrefix.Length + data.Length];
Data = data, Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
StringData = stringData, Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
IsBinary = isBinary
}); data = framed;
} }
catch (Exception ex)
{ if (_config.Protocol == ProtocolType.TCP)
if (_config.UseAesEncryption) {
{ await _stream.WriteAsync(data, 0, data.Length);
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error decrypting data" }); await _stream.FlushAsync();
} }
else else
{ {
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing received data" }); 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) public async Task SendAsync(string message)
{ {

View File

@ -243,103 +243,131 @@ namespace EonaCat.Connections
await ProcessReceivedDataAsync(client, result.Buffer); await ProcessReceivedDataAsync(client, result.Buffer);
} }
private async Task HandleClientCommunicationAsync(Connection client) private async Task HandleClientCommunicationAsync(Connection client)
{ {
var buffer = new byte[_config.BufferSize]; 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<int> 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) private async Task ProcessReceivedDataAsync(Connection client, byte[] data)
{ {
break; // Client disconnected 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) public async Task SendToClientAsync(string clientId, byte[] data)
{ {
@ -369,46 +397,54 @@ namespace EonaCat.Connections
await BroadcastAsync(Encoding.UTF8.GetBytes(message)); await BroadcastAsync(Encoding.UTF8.GetBytes(message));
} }
private async Task SendDataAsync(Connection client, byte[] data) private async Task SendDataAsync(Connection client, byte[] data)
{ {
try try
{ {
// Encrypt if AES encryption is enabled if (client.IsEncrypted && client.AesEncryption != null)
if (client.IsEncrypted && client.AesEncryption != null) {
{ // Encrypt payload
data = await EncryptDataAsync(data, client.AesEncryption); data = await EncryptDataAsync(data, client.AesEncryption);
}
// Prepend length for safe framing
if (_config.Protocol == ProtocolType.TCP) var lengthPrefix = BitConverter.GetBytes(data.Length);
{ if (BitConverter.IsLittleEndian)
await client.Stream.WriteAsync(data, 0, data.Length); Array.Reverse(lengthPrefix);
await client.Stream.FlushAsync();
} var framed = new byte[lengthPrefix.Length + data.Length];
else Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
{ Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
} data = framed; // replace data with framed payload
}
client.BytesSent += data.Length;
lock (_statsLock) if (_config.Protocol == ProtocolType.TCP)
{ {
_stats.BytesSent += data.Length; await client.Stream.WriteAsync(data, 0, data.Length);
_stats.MessagesSent++; await client.Stream.FlushAsync();
} }
} else
catch (Exception ex) {
{ await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
if (client.IsEncrypted) }
{
OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" }); client.BytesSent += data.Length;
} lock (_statsLock)
else {
{ _stats.BytesSent += data.Length;
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" }); _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<byte[]> EncryptDataAsync(byte[] data, Aes aes) private async Task<byte[]> EncryptDataAsync(byte[] data, Aes aes)
{ {
using (var encryptor = aes.CreateEncryptor()) using (var encryptor = aes.CreateEncryptor())