Initial version
This commit is contained in:
3
EonaCat.Sockets/EonaCat.Sockets.slnx
Normal file
3
EonaCat.Sockets/EonaCat.Sockets.slnx
Normal file
@@ -0,0 +1,3 @@
|
||||
<Solution>
|
||||
<Project Path="EonaCat.Sockets/EonaCat.Sockets.csproj" />
|
||||
</Solution>
|
||||
121
EonaCat.Sockets/EonaCat.Sockets/Client.cs
Normal file
121
EonaCat.Sockets/EonaCat.Sockets/Client.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using EonaCat.Sockets.Interfaces;
|
||||
using EonaCat.Sockets.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ProtocolType = EonaCat.Sockets.Models.ProtocolType;
|
||||
|
||||
namespace EonaCat.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Client which supports TCP, SSL-TCP, UDP.
|
||||
/// </summary>
|
||||
public class Client : IDisposable
|
||||
{
|
||||
private readonly NetConfiguration _config;
|
||||
private IConnection _connection;
|
||||
private int _disposed;
|
||||
|
||||
public bool IsConnected => _connection?.IsConnected ?? false;
|
||||
public string ConnectionId => _connection?.ConnectionId;
|
||||
public NetConfiguration Configuration => _config;
|
||||
|
||||
public Client(NetConfiguration config = null)
|
||||
{
|
||||
_config = config ?? new NetConfiguration();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
switch (_config.Protocol)
|
||||
{
|
||||
case ProtocolType.TCP:
|
||||
_connection = await ConnectTcpAsync(ct).ConfigureAwait(false);
|
||||
break;
|
||||
case ProtocolType.UDP:
|
||||
_connection = ConnectUdp();
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Protocol {_config.Protocol} not supported.");
|
||||
}
|
||||
_config.Logger?.Invoke($"Connected [{_config.Protocol}] → {_config.Host}:{_config.Port} id={_connection.ConnectionId}");
|
||||
}
|
||||
|
||||
private async Task<TcpConnection> ConnectTcpAsync(CancellationToken ct)
|
||||
{
|
||||
var tcpClient = new TcpClient();
|
||||
tcpClient.ReceiveTimeout = _config.ReceiveTimeoutMs;
|
||||
tcpClient.SendTimeout = _config.SendTimeoutMs;
|
||||
tcpClient.NoDelay = true;
|
||||
|
||||
await tcpClient.ConnectAsync(_config.Host, _config.Port).ConfigureAwait(false);
|
||||
|
||||
System.IO.Stream stream;
|
||||
|
||||
if (_config.UseSsl)
|
||||
{
|
||||
var sslStream = new SslStream(tcpClient.GetStream(), false,
|
||||
_config.CertificateValidationCallback ??
|
||||
(_config.AllowSelfSignedCertificates
|
||||
? (sender, certificate, chain, errors) => true
|
||||
: (RemoteCertificateValidationCallback)null));
|
||||
|
||||
await sslStream.AuthenticateAsClientAsync(_config.SslTargetHost ?? _config.Host).ConfigureAwait(false);
|
||||
stream = sslStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = tcpClient.GetStream();
|
||||
}
|
||||
|
||||
return new TcpConnection(tcpClient, stream);
|
||||
}
|
||||
|
||||
private UdpConnection ConnectUdp()
|
||||
{
|
||||
var udpClient = new UdpClient();
|
||||
udpClient.Client.ReceiveTimeout = _config.ReceiveTimeoutMs;
|
||||
udpClient.Client.SendTimeout = _config.SendTimeoutMs;
|
||||
var ep = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port);
|
||||
return new UdpConnection(udpClient, ep);
|
||||
}
|
||||
|
||||
public Task SendBytesAsync(byte[] data) => EnsureConnection().SendBytesAsync(data);
|
||||
public Task SendStringAsync(string msg) => EnsureConnection().SendStringAsync(msg);
|
||||
public Task<byte[]> ReceiveBytesAsync() => EnsureConnection().ReceiveBytesAsync();
|
||||
public Task<string> ReceiveStringAsync() => EnsureConnection().ReceiveStringAsync();
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_connection?.Close();
|
||||
_connection?.Dispose();
|
||||
_connection = null;
|
||||
_config.Logger?.Invoke("Disconnected.");
|
||||
}
|
||||
|
||||
private IConnection EnsureConnection()
|
||||
{
|
||||
var conn = _connection;
|
||||
if (conn == null || !conn.IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Not connected. Call ConnectAsync() first.");
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
EonaCat.Sockets/EonaCat.Sockets/ConnectionPool.cs
Normal file
66
EonaCat.Sockets/EonaCat.Sockets/ConnectionPool.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using EonaCat.Sockets.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection pool
|
||||
/// </summary>
|
||||
public class ConnectionPool<TConnection> where TConnection : IConnection
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, TConnection> _connections = new ConcurrentDictionary<string, TConnection>(StringComparer.Ordinal);
|
||||
|
||||
private long _totalAccepted;
|
||||
private long _totalDropped;
|
||||
private readonly int _maxConnections;
|
||||
|
||||
public ConnectionPool(int maxConnections)
|
||||
{
|
||||
_maxConnections = maxConnections;
|
||||
}
|
||||
|
||||
public int Count => _connections.Count;
|
||||
public long TotalAccepted => Interlocked.Read(ref _totalAccepted);
|
||||
public long TotalDropped => Interlocked.Read(ref _totalDropped);
|
||||
|
||||
public bool TryAdd(TConnection connection)
|
||||
{
|
||||
if (_connections.Count >= _maxConnections)
|
||||
{
|
||||
Interlocked.Increment(ref _totalDropped);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_connections.TryAdd(connection.ConnectionId, connection))
|
||||
{
|
||||
Interlocked.Increment(ref _totalAccepted);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRemove(string connectionId, out TConnection connection) => _connections.TryRemove(connectionId, out connection);
|
||||
|
||||
public bool TryGet(string connectionId, out TConnection connection) => _connections.TryGetValue(connectionId, out connection);
|
||||
|
||||
public void ForEach(Action<TConnection> action)
|
||||
{
|
||||
foreach (var keyValue in _connections)
|
||||
{
|
||||
action(keyValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var keyValue in _connections)
|
||||
{
|
||||
keyValue.Value.Close();
|
||||
keyValue.Value.Dispose();
|
||||
}
|
||||
_connections.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
EonaCat.Sockets/EonaCat.Sockets/EonaCat.Sockets.csproj
Normal file
7
EonaCat.Sockets/EonaCat.Sockets/EonaCat.Sockets.csproj
Normal file
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
16
EonaCat.Sockets/EonaCat.Sockets/Interfaces/IConnection.cs
Normal file
16
EonaCat.Sockets/EonaCat.Sockets/Interfaces/IConnection.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.Sockets.Interfaces
|
||||
{
|
||||
public interface IConnection : IDisposable
|
||||
{
|
||||
string ConnectionId { get; }
|
||||
bool IsConnected { get; }
|
||||
Task SendBytesAsync(byte[] data);
|
||||
Task SendStringAsync(string message);
|
||||
Task<byte[]> ReceiveBytesAsync();
|
||||
Task<string> ReceiveStringAsync();
|
||||
void Close();
|
||||
}
|
||||
}
|
||||
291
EonaCat.Sockets/EonaCat.Sockets/Interfaces/IServer.cs
Normal file
291
EonaCat.Sockets/EonaCat.Sockets/Interfaces/IServer.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using EonaCat.Sockets.Models;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ProtocolType = EonaCat.Sockets.Models.ProtocolType;
|
||||
|
||||
namespace EonaCat.Sockets.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// TCP/SSL/UDP server.
|
||||
/// </summary>
|
||||
public class Server : IDisposable
|
||||
{
|
||||
public event Func<IConnection, Task> ClientConnected;
|
||||
public event Func<IConnection, byte[], Task> DataReceived;
|
||||
public event Func<IConnection, string, Task> StringReceived;
|
||||
public event Func<IConnection, Exception, Task> ClientError;
|
||||
public event Func<IConnection, Task> ClientDisconnected;
|
||||
|
||||
private readonly NetConfiguration _config;
|
||||
private ConnectionPool<TcpConnection> _tcpPool;
|
||||
private TcpListener _tcpListener;
|
||||
private UdpClient _udpServer;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private int _disposed;
|
||||
|
||||
public int ActiveConnections => _tcpPool?.Count ?? 0;
|
||||
public bool IsRunning => _cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested;
|
||||
|
||||
public Server(NetConfiguration config = null)
|
||||
{
|
||||
_config = config ?? new NetConfiguration();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
switch (_config.Protocol)
|
||||
{
|
||||
case ProtocolType.TCP:
|
||||
StartTcp(_cancellationTokenSource.Token);
|
||||
break;
|
||||
case ProtocolType.UDP:
|
||||
StartUdp(_cancellationTokenSource.Token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTcp(CancellationToken cancellationToken)
|
||||
{
|
||||
_tcpPool = new ConnectionPool<TcpConnection>(_config.MaxConnections);
|
||||
|
||||
_tcpListener = new TcpListener(IPAddress.Any, _config.Port);
|
||||
|
||||
// Allow address reuse and dual-stack
|
||||
_tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||
|
||||
// Increase socket buffer sizes for high throughput
|
||||
_tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, _config.BufferSize);
|
||||
_tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, _config.BufferSize);
|
||||
|
||||
_tcpListener.Start(_config.BacklogSize);
|
||||
_config.Logger?.Invoke($"TCP Server listening on port {_config.Port} (max {_config.MaxConnections:N0} connections, SSL={_config.UseSsl})");
|
||||
|
||||
// Accept loop
|
||||
_ = AcceptLoopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
TcpClient tcpClient;
|
||||
|
||||
try
|
||||
{
|
||||
tcpClient = await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_config.Logger?.Invoke($"Accept error: {exception.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle each client on the thread pool
|
||||
_ = Task.Run(() => HandleTcpClientAsync(tcpClient, cancellationToken), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTcpClientAsync(TcpClient tcpClient, CancellationToken cancelationToken)
|
||||
{
|
||||
tcpClient.NoDelay = true;
|
||||
tcpClient.ReceiveTimeout = _config.ReceiveTimeoutMs;
|
||||
tcpClient.SendTimeout = _config.SendTimeoutMs;
|
||||
|
||||
System.IO.Stream stream;
|
||||
|
||||
try
|
||||
{
|
||||
if (_config.UseSsl)
|
||||
{
|
||||
var sslStream = new SslStream(tcpClient.GetStream(), false);
|
||||
await sslStream.AuthenticateAsServerAsync(
|
||||
_config.ServerCertificate,
|
||||
clientCertificateRequired: false,
|
||||
// No Tls 1.3 support for .net Framework 4.8
|
||||
enabledSslProtocols: SslProtocols.Tls12,
|
||||
checkCertificateRevocation: true).ConfigureAwait(false);
|
||||
stream = sslStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = tcpClient.GetStream();
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_config.Logger?.Invoke($"SSL handshake failed: {exception.Message}");
|
||||
tcpClient.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = new TcpConnection(tcpClient, stream);
|
||||
|
||||
if (!_tcpPool.TryAdd(connection))
|
||||
{
|
||||
_config.Logger?.Invoke($"Connection limit reached ({_config.MaxConnections:N0}). Dropping client.");
|
||||
connection.Close();
|
||||
connection.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (ClientConnected != null)
|
||||
{
|
||||
await ClientConnected(connection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ReceiveLoopAsync(connection, cancelationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (ClientError != null)
|
||||
{
|
||||
await ClientError(connection, exception).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_config.Logger?.Invoke($"Client {connection.ConnectionId} error: {exception.Message}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_tcpPool.TryRemove(connection.ConnectionId, out _);
|
||||
connection.Close();
|
||||
|
||||
if (ClientDisconnected != null)
|
||||
{
|
||||
await ClientDisconnected(connection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(TcpConnection conn, CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && conn.IsConnected)
|
||||
{
|
||||
byte[] data = await conn.ReceiveBytesAsync().ConfigureAwait(false);
|
||||
|
||||
if (DataReceived != null)
|
||||
{
|
||||
await DataReceived(conn, data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (StringReceived != null)
|
||||
{
|
||||
await StringReceived(conn, System.Text.Encoding.UTF8.GetString(data)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartUdp(CancellationToken ct)
|
||||
{
|
||||
_udpServer = new UdpClient(_config.Port);
|
||||
_config.Logger?.Invoke($"UDP Server listening on port {_config.Port}");
|
||||
_ = UdpReceiveLoopAsync(ct);
|
||||
}
|
||||
|
||||
private async Task UdpReceiveLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _udpServer.ReceiveAsync().ConfigureAwait(false);
|
||||
var conn = new UdpConnection(_udpServer, result.RemoteEndPoint);
|
||||
|
||||
if (DataReceived != null)
|
||||
{
|
||||
await DataReceived(conn, result.Buffer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (StringReceived != null)
|
||||
{
|
||||
await StringReceived(conn, System.Text.Encoding.UTF8.GetString(result.Buffer)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_config.Logger?.Invoke($"UDP receive error: {exception.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BroadcastBytesAsync(byte[] data)
|
||||
{
|
||||
if (_tcpPool == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tcpPool.ForEach(async c =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await c.SendBytesAsync(data).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task BroadcastStringAsync(string message) => BroadcastBytesAsync(System.Text.Encoding.UTF8.GetBytes(message));
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
try
|
||||
{
|
||||
_tcpListener?.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_udpServer?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
_tcpPool?.Clear();
|
||||
_config.Logger?.Invoke("Server stopped.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
Stop();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_udpServer?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
EonaCat.Sockets/EonaCat.Sockets/Models/Connection.cs
Normal file
37
EonaCat.Sockets/EonaCat.Sockets/Models/Connection.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace EonaCat.Sockets.Models
|
||||
{
|
||||
public enum ProtocolType { TCP, UDP }
|
||||
|
||||
public class NetConfiguration
|
||||
{
|
||||
// Connection limits
|
||||
public int MaxConnections { get; set; } = 2_000_000;
|
||||
public int BacklogSize { get; set; } = 10_000;
|
||||
|
||||
// Protocol
|
||||
public ProtocolType Protocol { get; set; } = ProtocolType.TCP;
|
||||
|
||||
// Network
|
||||
public string Host { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 9000;
|
||||
public int BufferSize { get; set; } = 65536;
|
||||
public int ReceiveTimeoutMs { get; set; } = 30_000;
|
||||
public int SendTimeoutMs { get; set; } = 30_000;
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
// SSL
|
||||
public bool UseSsl { get; set; } = false;
|
||||
public X509Certificate2 ServerCertificate { get; set; }
|
||||
public string SslTargetHost { get; set; }
|
||||
public bool AllowSelfSignedCertificates { get; set; } = false;
|
||||
public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; }
|
||||
|
||||
public int IoThreads { get; set; } = Environment.ProcessorCount * 2;
|
||||
|
||||
public Action<string> Logger { get; set; } = message => Console.WriteLine($"[EonaCat.Sockets] {message}");
|
||||
}
|
||||
}
|
||||
142
EonaCat.Sockets/EonaCat.Sockets/TcpConnection.cs
Normal file
142
EonaCat.Sockets/EonaCat.Sockets/TcpConnection.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using EonaCat.Sockets.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single TCP or SSL connection.
|
||||
/// </summary>
|
||||
public class TcpConnection : IConnection
|
||||
{
|
||||
private readonly TcpClient _client;
|
||||
private readonly Stream _stream; // NetworkStream or SslStream
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim _recvLock = new SemaphoreSlim(1, 1);
|
||||
private int _disposed;
|
||||
|
||||
public string ConnectionId { get; }
|
||||
public bool IsConnected => _client?.Connected ?? false;
|
||||
|
||||
public TcpConnection(TcpClient client, Stream stream, string connectionId = null)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
ConnectionId = connectionId ?? Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
public async Task SendBytesAsync(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
await _sendLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// 4-byte length prefix (big-endian) then payload
|
||||
var lenBytes = BitConverter.GetBytes(data.Length);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(lenBytes);
|
||||
}
|
||||
|
||||
await _stream.WriteAsync(lenBytes, 0, 4).ConfigureAwait(false);
|
||||
await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
|
||||
await _stream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendStringAsync(string message) => await SendBytesAsync(Encoding.UTF8.GetBytes(message ?? string.Empty)).ConfigureAwait(false);
|
||||
|
||||
public async Task<byte[]> ReceiveBytesAsync()
|
||||
{
|
||||
await _recvLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var bufferLength = new byte[4];
|
||||
await ReadExactAsync(bufferLength, 4).ConfigureAwait(false);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(bufferLength);
|
||||
}
|
||||
|
||||
int length = BitConverter.ToInt32(bufferLength, 0);
|
||||
|
||||
// 128 MB safety limit
|
||||
if (length < 0 || length > 128 * 1024 * 1024)
|
||||
{
|
||||
throw new InvalidDataException($"Invalid message length: {length}");
|
||||
}
|
||||
|
||||
var data = new byte[length];
|
||||
await ReadExactAsync(data, length).ConfigureAwait(false);
|
||||
return data;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_recvLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveStringAsync() => Encoding.UTF8.GetString(await ReceiveBytesAsync().ConfigureAwait(false));
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, int count)
|
||||
{
|
||||
int offset = 0;
|
||||
while (offset < count)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer, offset, count - offset).ConfigureAwait(false);
|
||||
if (read == 0)
|
||||
{
|
||||
throw new EndOfStreamException("Connection closed by remote host.");
|
||||
}
|
||||
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_stream?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_client?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
_sendLock.Dispose();
|
||||
_recvLock.Dispose();
|
||||
_stream?.Dispose();
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
84
EonaCat.Sockets/EonaCat.Sockets/UdpConnection.cs
Normal file
84
EonaCat.Sockets/EonaCat.Sockets/UdpConnection.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using EonaCat.Sockets.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// UDP connection wrapper
|
||||
/// Note: UDP packets are limited to ~65507 bytes per datagram.
|
||||
/// </summary>
|
||||
public class UdpConnection : IConnection
|
||||
{
|
||||
private readonly UdpClient _udpClient;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
private int _disposed;
|
||||
|
||||
public string ConnectionId { get; }
|
||||
public bool IsConnected => !(_disposed == 1);
|
||||
|
||||
public UdpConnection(UdpClient udpClient, IPEndPoint remoteEndPoint, string connectionId = null)
|
||||
{
|
||||
_udpClient = udpClient ?? throw new ArgumentNullException(nameof(udpClient));
|
||||
_remoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
ConnectionId = connectionId ?? $"udp-{remoteEndPoint}";
|
||||
}
|
||||
|
||||
public async Task SendBytesAsync(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
await _sendLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _udpClient.SendAsync(data, data.Length, _remoteEndPoint).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendStringAsync(string message) => await SendBytesAsync(Encoding.UTF8.GetBytes(message ?? string.Empty)).ConfigureAwait(false);
|
||||
|
||||
public async Task<byte[]> ReceiveBytesAsync()
|
||||
{
|
||||
var result = await _udpClient.ReceiveAsync().ConfigureAwait(false);
|
||||
return result.Buffer;
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveStringAsync() => Encoding.UTF8.GetString(await ReceiveBytesAsync().ConfigureAwait(false));
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_udpClient?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
_sendLock.Dispose();
|
||||
_udpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user