commit 0793d3083bc8126a125b6162f42020cbcf9e06a4
parent a8d2d1c315a0f0643d0316e9161e7a6dccc653a2
Author: Mikolaj Lenczewski <33129490+EnderRifter@users.noreply.github.com>
Date: Tue, 3 Mar 2020 22:50:11 +0000
TCP (sorta) works, UDP (sorta) works. Disconnect methods need work.
Diffstat:
7 files changed, 839 insertions(+), 449 deletions(-)
diff --git a/NetSharp/NetSharp/Connection.cs b/NetSharp/NetSharp/Connection.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
@@ -6,11 +7,11 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
+using Microsoft.Extensions.ObjectPool;
using NetSharp.Deprecated;
using NetSharp.Logging;
using NetSharp.Packets;
using NetSharp.Pipelines;
-using NetSharp.Sockets;
using NetSharp.Utils;
namespace NetSharp
@@ -18,17 +19,9 @@ namespace NetSharp
/// <summary>
/// Encapsulates a connection capable of receiving packets and responding to them with registered packet handlers.
/// </summary>
- public class Connection : IDisposable
+ public sealed partial class Connection : IDisposable
{
- /// <summary>
- /// Represents any remote endpoint for datagram operations.
- /// </summary>
- private static readonly EndPoint AnyRemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
-
- private readonly SocketAcceptor acceptor;
-
- private readonly ConcurrentDictionary<EndPoint, DatagramClientArgs> datagramConnections;
- private readonly Socket datagramSocket;
+ private readonly ConcurrentDictionary<EndPoint, SocketAsyncEventArgs> datagramConnections;
private readonly Channel<(EndPoint origin, Memory<byte> packet)> incomingPacketChannel;
/// <summary>
@@ -36,7 +29,11 @@ namespace NetSharp
/// </summary>
private readonly PacketPipeline<Memory<byte>, Memory<byte>, NetworkPacket> incomingPacketPipeline;
- private readonly SocketReader listener;
+ /// <summary>
+ /// Lock synchronisation object for the <see cref="logger"/> variable.
+ /// </summary>
+ private readonly object loggerLockObject = new object();
+
private readonly Channel<(EndPoint destination, NetworkPacket packet)> outgoingPacketChannel;
/// <summary>
@@ -45,12 +42,18 @@ namespace NetSharp
private readonly PacketPipeline<NetworkPacket, Memory<byte>, Memory<byte>> outgoingPacketPipeline;
private readonly Channel<(EndPoint origin, IRequestPacket request)> requestChannel;
- private readonly CancellationTokenSource serverShutdownTokenSource;
- private readonly ConcurrentDictionary<EndPoint, StreamClientArgs> streamConnections;
- private readonly Socket streamSocket;
+ /// <summary>
+ /// Cancellation token which allows observing the shutdown of the server. It is set when <see cref="ShutdownServer"/> is called.
+ /// </summary>
+ private readonly CancellationToken ServerShutdownToken;
- private readonly SocketWriter transmitter;
+ private readonly ConcurrentDictionary<EndPoint, SocketAsyncEventArgs> streamConnections;
+
+ /// <summary>
+ /// A logger object allowing for writing debug messages to an output stream.
+ /// </summary>
+ private Logger logger;
/// <summary>
/// Destroys a <see cref="Connection"/> class instance, freeing all managed resources.
@@ -66,10 +69,13 @@ namespace NetSharp
logger.LogMessage("Started stream acceptor task.");
- async Task StreamListenerWork(object clientSocketObj)
+ async Task StreamListenerWork(object clientArgsObj)
{
- Socket clientSocket = (Socket)clientSocketObj;
- logger.LogMessage("Started stream listener task.");
+ SocketAsyncEventArgs clientArgs = (SocketAsyncEventArgs)clientArgsObj;
+ Socket clientSocket = clientArgs.AcceptSocket;
+ EndPoint clientEndPoint = clientSocket.RemoteEndPoint;
+
+ logger.LogMessage($"Client handler started for {clientEndPoint}");
while (!cancellationToken.IsCancellationRequested)
{
@@ -78,26 +84,41 @@ namespace NetSharp
Memory<byte> receiveBufferMemory = new Memory<byte>(receiveBuffer);
TransmissionResult result =
- await listener.ReceiveAsync(clientSocket, SocketFlags.None, receiveBufferMemory, cancellationToken);
+ await DoReceiveFromAsync(clientSocket, clientEndPoint, SocketFlags.None, receiveBufferMemory, cancellationToken);
+
+ if (result.Count == 0)
+ {
+ break;
+ }
await incomingPacketChannel.Writer.WriteAsync((result.RemoteEndPoint, receiveBufferMemory),
cancellationToken);
}
- logger.LogMessage("Stopped stream listener task.");
+ logger.LogMessage($"Client handler stopped for {clientEndPoint}");
+
+ clientSocket.Shutdown(SocketShutdown.Both);
+ clientSocket.Close(1);
}
+ streamSocket.Listen(MaximumConnectionBacklog);
+
while (!cancellationToken.IsCancellationRequested)
{
- Socket clientSocket = await acceptor.AcceptAsync(streamSocket, cancellationToken);
+ SocketAsyncEventArgs clientArgs = await DoAcceptAsync(streamSocket, cancellationToken);
+
+ Socket clientSocket = clientArgs.AcceptSocket;
if (!streamConnections.ContainsKey(clientSocket.RemoteEndPoint))
{
- streamConnections[clientSocket.RemoteEndPoint] = new StreamClientArgs();
- }
+ streamConnections[clientSocket.RemoteEndPoint] = clientArgs;
- await Task.Factory.StartNew(StreamListenerWork, clientSocket, ServerShutdownToken,
- TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ await Task.Factory.StartNew(StreamListenerWork, clientArgs, ServerShutdownToken);
+ }
+ else
+ {
+ logger.LogWarning($"Accepted duplicate connection from {clientSocket.RemoteEndPoint}");
+ }
}
logger.LogMessage("Stopped stream acceptor task.");
@@ -111,20 +132,24 @@ namespace NetSharp
while (!cancellationToken.IsCancellationRequested)
{
+ SocketAsyncEventArgs args = clientSocketArgsPool.Get();
+
// TODO: implement receive buffer pooling
byte[] receiveBuffer = new byte[NetworkPacket.PacketSize];
Memory<byte> receiveBufferMemory = new Memory<byte>(receiveBuffer);
TransmissionResult result =
- await listener.ReceiveFromAsync(datagramSocket, AnyRemoteEndPoint, SocketFlags.None,
+ await DoReceiveFromAsync(datagramSocket, AnyRemoteEndPoint, SocketFlags.None,
receiveBufferMemory, cancellationToken);
if (!datagramConnections.ContainsKey(result.RemoteEndPoint))
{
- datagramConnections[result.RemoteEndPoint] = new DatagramClientArgs();
+ datagramConnections[result.RemoteEndPoint] = args;
}
await incomingPacketChannel.Writer.WriteAsync((result.RemoteEndPoint, receiveBufferMemory), cancellationToken);
+
+ clientSocketArgsPool.Return(args);
}
logger.LogMessage("Stopped datagram listener task.");
@@ -167,14 +192,14 @@ namespace NetSharp
if (datagramConnections.ContainsKey(destination))
{
- await transmitter.SendToAsync(datagramSocket, destination,
+ await DoSendToAsync(datagramSocket, destination,
SocketFlags.None, serialisedResponse, cancellationToken);
}
else if (streamConnections.ContainsKey(destination))
{
- StreamClientArgs streamClientArgs = streamConnections[destination];
+ SocketAsyncEventArgs streamClientArgs = streamConnections[destination];
- await transmitter.SendAsync(streamClientArgs.ClientSocket,
+ await DoSendToAsync(streamClientArgs.AcceptSocket, destination,
SocketFlags.None, serialisedResponse, cancellationToken);
}
else
@@ -206,71 +231,66 @@ namespace NetSharp
logger.LogMessage("Stopped request handler invocation task.");
}
- private readonly struct DatagramClientArgs
+ internal Connection(
+ PacketPipeline<Memory<byte>, Memory<byte>, NetworkPacket> incomingPacketPipeline,
+ PacketPipeline<NetworkPacket, Memory<byte>, Memory<byte>> outgoingPacketPipeline,
+ int objectPoolSize = 10, bool preallocateBuffers = false, Stream? loggingStream = default,
+ LogLevel minimumLoggedSeverity = LogLevel.Info)
{
- public readonly EndPoint ClientEndPoint;
+ serverShutdownTokenSource = new CancellationTokenSource();
+ ServerShutdownToken = serverShutdownTokenSource.Token;
- public DatagramClientArgs(EndPoint clientEndPoint)
- {
- ClientEndPoint = clientEndPoint;
- }
- }
+ streamSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ datagramSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- private readonly struct StreamClientArgs
- {
- public readonly Socket ClientSocket;
+ sendToBufferPool = ArrayPool<byte>.Create(NetworkPacket.PacketSize, objectPoolSize);
+ receiveFromBufferPool = ArrayPool<byte>.Create(NetworkPacket.PacketSize, objectPoolSize);
- public StreamClientArgs(Socket clientSocket)
- {
- ClientSocket = clientSocket;
- }
- }
+ clientSocketArgsPool =
+ new LeakTrackingObjectPool<SocketAsyncEventArgs>(
+ new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(),
+ objectPoolSize));
- /// <summary>
- /// Lock synchronisation object for the <see cref="logger"/> variable.
- /// </summary>
- protected readonly object loggerLockObject = new object();
+ acceptArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize);
+ connectArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize);
+ disconnectArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize);
+ receiveArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize);
+ sendArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize);
- /// <summary>
- /// Cancellation token which allows observing the shutdown of the server. It is set when <see cref="ShutdownServer"/> is called.
- /// </summary>
- protected readonly CancellationToken ServerShutdownToken;
+ for (int i = 0; i < objectPoolSize; i++)
+ {
+ SocketAsyncEventArgs clientArgs = new SocketAsyncEventArgs();
+ clientArgs.Completed += HandleIOCompleted;
+ clientSocketArgsPool.Return(clientArgs);
- /// <summary>
- /// A logger object allowing for writing debug messages to an output stream.
- /// </summary>
- protected Logger logger;
+ SocketAsyncEventArgs acceptArgs = new SocketAsyncEventArgs();
+ acceptArgs.Completed += HandleIOCompleted;
+ acceptArgsPool.Return(acceptArgs);
- /// <summary>
- /// Disposes of the managed and unmanaged resources held by this instance.
- /// </summary>
- /// <param name="disposing">Whether this method is called by <see cref="Dispose()"/> or by the finaliser.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- streamSocket.Dispose();
- }
- }
+ SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs();
+ connectArgs.Completed += HandleIOCompleted;
+ connectArgsPool.Return(connectArgs);
- internal Connection(
- PacketPipeline<Memory<byte>, Memory<byte>, NetworkPacket> incomingPacketPipeline,
- PacketPipeline<NetworkPacket, Memory<byte>, Memory<byte>> outgoingPacketPipeline,
- int objectPoolSize = 10, bool preallocateBuffers = false, Stream? loggingStream = default,
- LogLevel minimumLoggedSeverity = LogLevel.Info)
- {
- serverShutdownTokenSource = new CancellationTokenSource();
- ServerShutdownToken = serverShutdownTokenSource.Token;
+ SocketAsyncEventArgs disconnectArgs = new SocketAsyncEventArgs();
+ disconnectArgs.Completed += HandleIOCompleted;
+ disconnectArgsPool.Return(disconnectArgs);
- streamSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- datagramSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
+ receiveArgs.Completed += HandleIOCompleted;
+ receiveArgsPool.Return(receiveArgs);
+
+ SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
+ sendArgs.Completed += HandleIOCompleted;
+ sendArgsPool.Return(sendArgs);
+ }
- acceptor = new SocketAcceptor(objectPoolSize);
- listener = new SocketReader(NetworkPacket.PacketSize, objectPoolSize, preallocateBuffers);
- transmitter = new SocketWriter(NetworkPacket.PacketSize, objectPoolSize, preallocateBuffers);
+ if (preallocateBuffers)
+ {
+ //TODO: Preallocate buffers someday
+ }
- streamConnections = new ConcurrentDictionary<EndPoint, StreamClientArgs>();
- datagramConnections = new ConcurrentDictionary<EndPoint, DatagramClientArgs>();
+ streamConnections = new ConcurrentDictionary<EndPoint, SocketAsyncEventArgs>();
+ datagramConnections = new ConcurrentDictionary<EndPoint, SocketAsyncEventArgs>();
this.incomingPacketPipeline = incomingPacketPipeline;
BoundedChannelOptions incomingChannelOptions = new BoundedChannelOptions(MaximumPacketBacklog)
@@ -302,37 +322,6 @@ namespace NetSharp
}
/// <summary>
- /// The maximum number of packets that will be stored before older packets start to be dropped.
- /// </summary>
- /// TODO change this to a configurable builder option
- public const int MaximumPacketBacklog = 64;
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- public Task<TransmissionResult> ReceiveAsync(Memory<byte> inputBuffer, SocketFlags flags, TimeSpan timeout)
- {
- using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
- using CancellationTokenSource cts =
- CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
-
- return listener.ReceiveAsync(streamSocket, flags, inputBuffer, cts.Token);
- }
-
- public Task<TransmissionResult> ReceiveFromAsync(EndPoint remoteEndPoint, Memory<byte> inputBuffer, SocketFlags flags, TimeSpan timeout)
- {
- using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
- using CancellationTokenSource cts =
- CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
-
- return listener.ReceiveFromAsync(datagramSocket, remoteEndPoint, flags, inputBuffer, cts.Token);
- }
-
- /// <summary>
/// Makes the connection listen for incoming request packets, and handle them according to registered packet handler delegates.
/// This work can be cancelled by calling <see cref="ShutdownServer"/>.
/// </summary>
@@ -368,38 +357,6 @@ namespace NetSharp
incomingPacketHandlerThread, requestHandlerInvocationThread, outgoingPacketHandlerThread);
}
- public ValueTask<int> SendAsync(Memory<byte> outputBuffer, SocketFlags flags, TimeSpan timeout)
- {
- using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
- using CancellationTokenSource cts =
- CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
-
- return transmitter.SendAsync(streamSocket, flags, outputBuffer, cts.Token);
- }
-
- public ValueTask<int> SendToAsync(EndPoint remoteEndPoint, Memory<byte> outputBuffer, SocketFlags flags, TimeSpan timeout)
- {
- using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
- using CancellationTokenSource cts =
- CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
-
- return transmitter.SendToAsync(datagramSocket, remoteEndPoint, flags, outputBuffer, cts.Token);
- }
-
- /// <summary>
- /// Configures the logger to log messages to the given stream (or to <see cref="Stream.Null"/> if <c>null</c>) and
- /// to only log messages that are of severity <paramref name="minimumLoggedSeverity"/> or higher.
- /// </summary>
- /// <param name="loggingStream">The stream to which messages will be logged.</param>
- /// <param name="minimumLoggedSeverity">The minimum severity a message must be to be logged.</param>
- public void SetLoggingStream(Stream? loggingStream, LogLevel minimumLoggedSeverity = LogLevel.Info)
- {
- lock (loggerLockObject)
- {
- logger = new Logger(loggingStream ?? Stream.Null, minimumLoggedSeverity);
- }
- }
-
/// <summary>
/// Shuts down the connection, and releases managed and unmanaged resources.
/// </summary>
@@ -408,49 +365,5 @@ namespace NetSharp
logger.LogMessage("Signalling shutdown to all client connection handlers...");
serverShutdownTokenSource.Cancel();
}
-
- /// <summary>
- /// Attempts to synchronously bind the underlying socket to the given local endpoint. Blocks.
- /// If the timeout is exceeded the binding attempt is aborted and the method returns false.
- /// </summary>
- /// <param name="localEndPoint">The local endpoint to bind to.</param>
- /// <param name="timeout">The timeout within which to attempt the binding.</param>
- /// <returns>Whether the binding was successful or not.</returns>
- public bool TryBind(EndPoint localEndPoint, TimeSpan timeout) =>
- TryBindAsync(localEndPoint, timeout).Result;
-
- /// <summary>
- /// Attempts to asynchronously bind the underlying socket to the given local endpoint. Does not block.
- /// If the timeout is exceeded the binding attempt is aborted and the method returns false.
- /// </summary>
- /// <param name="localEndPoint">The local endpoint to bind to.</param>
- /// <param name="timeout">The timeout within which to attempt the binding.</param>
- /// <returns>Whether the binding was successful or not.</returns>
- public async Task<bool> TryBindAsync(EndPoint localEndPoint, TimeSpan timeout)
- {
- using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
- using CancellationTokenSource cts =
- CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
-
- try
- {
- return await Task.Run(() =>
- {
- streamSocket.Bind(localEndPoint);
- datagramSocket.Bind(localEndPoint);
-
- return true;
- }, cts.Token);
- }
- catch (TaskCanceledException)
- {
- return false;
- }
- catch (SocketException ex)
- {
- logger.LogException($"Socket exception on binding socket to {localEndPoint}:", ex);
- return false;
- }
- }
}
}
\ No newline at end of file
diff --git a/NetSharp/NetSharp/ConnectionBase.cs b/NetSharp/NetSharp/ConnectionBase.cs
@@ -0,0 +1,594 @@
+using System;
+using System.Buffers;
+using System.Data;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.ObjectPool;
+using NetSharp.Logging;
+using NetSharp.Packets;
+using NetSharp.Utils;
+
+namespace NetSharp
+{
+ /// <summary>
+ /// Implements low-level network access on top of which the rest of the connection is built upon.
+ /// </summary>
+ public sealed partial class Connection : IDisposable
+ {
+ /// <summary>
+ /// Represents any remote endpoint for datagram operations.
+ /// </summary>
+ private static readonly EndPoint AnyRemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
+
+ private readonly ObjectPool<SocketAsyncEventArgs> acceptArgsPool;
+
+ private readonly ObjectPool<SocketAsyncEventArgs> clientSocketArgsPool;
+
+ private readonly ObjectPool<SocketAsyncEventArgs> connectArgsPool;
+
+ private readonly Socket datagramSocket;
+
+ private readonly ObjectPool<SocketAsyncEventArgs> disconnectArgsPool;
+
+ private readonly ObjectPool<SocketAsyncEventArgs> receiveArgsPool;
+
+ private readonly ArrayPool<byte> receiveFromBufferPool;
+
+ private readonly ObjectPool<SocketAsyncEventArgs> sendArgsPool;
+
+ private readonly ArrayPool<byte> sendToBufferPool;
+
+ private readonly CancellationTokenSource serverShutdownTokenSource;
+
+ private readonly Socket streamSocket;
+
+ /// <summary>
+ /// Socket async event args object used when the connection instance is used as a client. It is set when a
+ /// call to <see cref="TryConnectAsync"/> is made.
+ /// </summary>
+ private volatile SocketAsyncEventArgs? connectionAsyncEventArgs;
+
+ /// <summary>
+ /// Disposes of the managed and unmanaged resources held by this instance.
+ /// </summary>
+ /// <param name="disposing">Whether this method is called by <see cref="Dispose()"/> or by the finaliser.</param>
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ serverShutdownTokenSource.Cancel();
+ serverShutdownTokenSource.Dispose();
+
+ streamSocket.Dispose();
+ datagramSocket.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Provides an awaitable wrapper around an asynchronous socket accept operation.
+ /// </summary>
+ /// <param name="serverSocket">The socket which should be used to accept an incoming connection attempt.</param>
+ /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ /// <returns>The accepted socket.</returns>
+ private Task<SocketAsyncEventArgs> DoAcceptAsync(Socket serverSocket, CancellationToken cancellationToken = default)
+ {
+ TaskCompletionSource<SocketAsyncEventArgs> tcs = new TaskCompletionSource<SocketAsyncEventArgs>();
+
+ SocketAsyncEventArgs acceptArgs = acceptArgsPool.Get();
+ acceptArgs.AcceptSocket = null;
+ acceptArgs.UserToken = new AsyncAcceptToken(tcs, cancellationToken);
+
+ // if the accept operation doesn't complete synchronously, return the awaitable task
+ return serverSocket.AcceptAsync(acceptArgs) ? tcs.Task : Task.FromResult(acceptArgs);
+ }
+
+ /// <summary>
+ /// Provides an awaitable wrapper around an asynchronous socket connect operation.
+ /// </summary>
+ /// <param name="disconnectedSocket">The socket which should asynchronously connect to the remote endpoint.</param>
+ /// <param name="remoteEndPoint">The remote endpoint to which the socket should connect.</param>
+ /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ private Task<SocketAsyncEventArgs> DoConnectAsync(Socket disconnectedSocket, EndPoint remoteEndPoint, CancellationToken cancellationToken = default)
+ {
+ TaskCompletionSource<SocketAsyncEventArgs> tcs = new TaskCompletionSource<SocketAsyncEventArgs>();
+
+ SocketAsyncEventArgs connectArgs = connectArgsPool.Get();
+ connectArgs.RemoteEndPoint = remoteEndPoint;
+ connectArgs.UserToken = new AsyncConnectToken(tcs, cancellationToken);
+
+ // if the connect operation doesn't complete synchronously, return the awaitable task
+ return disconnectedSocket.ConnectAsync(connectArgs) ? tcs.Task : Task.FromResult(connectArgs);
+ }
+
+ /// <summary>
+ /// Provides an awaitable wrapper around an asynchronous socket disconnect operation.
+ /// </summary>
+ /// <param name="connectedSocket">The socket which should asynchronously disconnect from its remote endpoint.</param>
+ /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ private Task DoDisconnectAsync(Socket connectedSocket, CancellationToken cancellationToken = default)
+ {
+ TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
+
+ SocketAsyncEventArgs disconnectArgs = disconnectArgsPool.Get();
+ disconnectArgs.DisconnectReuseSocket = true;
+ disconnectArgs.UserToken = new AsyncDisconnectToken(tcs, cancellationToken);
+
+ // if the disconnect operation doesn't complete synchronously, return the awaitable task
+ if (connectedSocket.DisconnectAsync(disconnectArgs))
+ {
+ return tcs.Task;
+ }
+
+ disconnectArgsPool.Return(disconnectArgs);
+
+ return Task.CompletedTask;
+ }
+
+ /// <summary>
+ /// Provides an awaitable wrapper around an asynchronous socket receive operation.
+ /// </summary>
+ /// <param name="listenerSocket">The socket which should receive data from the remote endpoint.</param>
+ /// <param name="remoteEndPoint">The remove endpoint from which data should be received.</param>
+ /// <param name="socketFlags">The socket flags associated with the receive operation.</param>
+ /// <param name="inputBuffer">The memory buffer into which received data will be stored.</param>
+ /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ /// <returns>The result of the receive operation from the remote endpoint.</returns>
+ private Task<TransmissionResult> DoReceiveFromAsync(Socket listenerSocket, EndPoint remoteEndPoint, SocketFlags socketFlags,
+ Memory<byte> inputBuffer, CancellationToken cancellationToken = default)
+ {
+ TaskCompletionSource<TransmissionResult> tcs = new TaskCompletionSource<TransmissionResult>();
+
+ byte[] rentedReceiveFromBuffer = receiveFromBufferPool.Rent(NetworkPacket.PacketSize);
+ Memory<byte> rentedReceiveFromBufferMemory = new Memory<byte>(rentedReceiveFromBuffer);
+
+ SocketAsyncEventArgs clientArgs = receiveArgsPool.Get();
+ clientArgs.SetBuffer(rentedReceiveFromBufferMemory);
+ clientArgs.SocketFlags = socketFlags;
+ clientArgs.RemoteEndPoint = remoteEndPoint;
+ clientArgs.UserToken = new AsyncReadToken(rentedReceiveFromBuffer, inputBuffer, tcs, cancellationToken);
+
+ // if the receive operation doesn't complete synchronously, returns the awaitable task
+ if (listenerSocket.ReceiveFromAsync(clientArgs)) return tcs.Task;
+
+ clientArgs.MemoryBuffer.CopyTo(inputBuffer);
+
+ TransmissionResult result = new TransmissionResult(clientArgs);
+
+ receiveFromBufferPool.Return(rentedReceiveFromBuffer, true);
+ receiveArgsPool.Return(clientArgs);
+
+ return Task.FromResult(result);
+ }
+
+ /// <summary>
+ /// Provides an awaitable wrapper around an asynchronous socket send operation.
+ /// </summary>
+ /// <param name="transmitterSocket">The socket which should send the data to the remote endpoint.</param>
+ /// <param name="remoteEndPoint">The remote endpoint to which data should be written.</param>
+ /// <param name="socketFlags">The socket flags associated with the send operation.</param>
+ /// <param name="outputBuffer">The data buffer which should be sent.</param>
+ /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ /// <returns>The result of the send operation to the remote endpoint.</returns>
+ private ValueTask<int> DoSendToAsync(Socket transmitterSocket, EndPoint remoteEndPoint, SocketFlags socketFlags,
+ Memory<byte> outputBuffer, CancellationToken cancellationToken = default)
+ {
+ TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
+
+ byte[] rentedSendToBuffer = sendToBufferPool.Rent(NetworkPacket.PacketSize);
+ Memory<byte> rentedSendToBufferMemory = new Memory<byte>(rentedSendToBuffer);
+
+ outputBuffer.CopyTo(rentedSendToBufferMemory);
+
+ SocketAsyncEventArgs clientArgs = sendArgsPool.Get();
+ clientArgs.SetBuffer(rentedSendToBufferMemory);
+ clientArgs.SocketFlags = socketFlags;
+ clientArgs.RemoteEndPoint = remoteEndPoint;
+ clientArgs.UserToken = new AsyncWriteToken(rentedSendToBuffer, tcs, cancellationToken);
+
+ /* NOT WORKING, NEED SOLUTION AT SOME POINT!!!
+ // register cleanup action for when the cancellation token is thrown
+ cancellationToken.Register(() =>
+ {
+ tcs.SetCanceled();
+
+ sendBufferPool.Return(rentedSendToBuffer, true);
+
+ //TODO this is probably a hideous solution. find a better one
+ args.Completed -= HandleIOCompleted;
+ args.Dispose();
+
+ SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs();
+ newArgs.Completed += HandleIOCompleted;
+ sendAsyncEventArgsPool.Return(newArgs);
+ });
+ */
+
+ // if the send operation doesn't complete synchronously, return the awaitable task
+ if (transmitterSocket.SendToAsync(clientArgs)) return new ValueTask<int>(tcs.Task);
+
+ int result = clientArgs.BytesTransferred;
+
+ sendToBufferPool.Return(rentedSendToBuffer, true);
+ sendArgsPool.Return(clientArgs);
+
+ return new ValueTask<int>(result);
+ }
+
+ private void HandleIOCompleted(object? sender, SocketAsyncEventArgs args)
+ {
+ switch (args.LastOperation)
+ {
+ case SocketAsyncOperation.Accept:
+ AsyncAcceptToken asyncAcceptToken = (AsyncAcceptToken)args.UserToken;
+
+ if (asyncAcceptToken.CancellationToken.IsCancellationRequested)
+ {
+ asyncAcceptToken.CompletionSource.SetCanceled();
+ }
+ else
+ {
+ if (args.SocketError != SocketError.Success)
+ {
+ asyncAcceptToken.CompletionSource.SetException(
+ new SocketException((int)args.SocketError));
+ }
+ else
+ {
+ asyncAcceptToken.CompletionSource.SetResult(args);
+ }
+ }
+
+ break;
+
+ case SocketAsyncOperation.Connect:
+ AsyncConnectToken asyncConnectToken = (AsyncConnectToken)args.UserToken;
+
+ if (asyncConnectToken.CancellationToken.IsCancellationRequested)
+ {
+ asyncConnectToken.CompletionSource.SetCanceled();
+ }
+ else
+ {
+ if (args.SocketError != SocketError.Success)
+ {
+ asyncConnectToken.CompletionSource.SetException(
+ new SocketException((int)args.SocketError));
+ }
+ else
+ {
+ asyncConnectToken.CompletionSource.SetResult(args);
+ }
+ }
+
+ break;
+
+ case SocketAsyncOperation.Disconnect:
+ AsyncDisconnectToken asyncDisconnectToken = (AsyncDisconnectToken)args.UserToken;
+
+ if (asyncDisconnectToken.CancellationToken.IsCancellationRequested)
+ {
+ asyncDisconnectToken.CompletionSource.SetCanceled();
+ }
+ else
+ {
+ if (args.SocketError != SocketError.Success)
+ {
+ asyncDisconnectToken.CompletionSource.SetException(
+ new SocketException((int)args.SocketError));
+ }
+ else
+ {
+ asyncDisconnectToken.CompletionSource.SetResult(true);
+ }
+ }
+
+ disconnectArgsPool.Return(args);
+
+ break;
+
+ case SocketAsyncOperation.SendTo:
+ AsyncWriteToken asyncSendToToken = (AsyncWriteToken)args.UserToken;
+
+ if (asyncSendToToken.CancellationToken.IsCancellationRequested)
+ {
+ asyncSendToToken.CompletionSource.SetCanceled();
+ }
+ else
+ {
+ if (args.SocketError != SocketError.Success)
+ {
+ asyncSendToToken.CompletionSource.SetException(
+ new SocketException((int)args.SocketError));
+ }
+ else
+ {
+ asyncSendToToken.CompletionSource.SetResult(args.BytesTransferred);
+ }
+ }
+
+ sendToBufferPool.Return(asyncSendToToken.RentedBuffer, true);
+ sendArgsPool.Return(args);
+
+ break;
+
+ case SocketAsyncOperation.ReceiveFrom:
+ AsyncReadToken asyncReceiveFromToken = (AsyncReadToken)args.UserToken;
+
+ if (asyncReceiveFromToken.CancellationToken.IsCancellationRequested)
+ {
+ asyncReceiveFromToken.CompletionSource.SetCanceled();
+ }
+ else
+ {
+ if (args.SocketError != SocketError.Success)
+ {
+ asyncReceiveFromToken.CompletionSource.SetException(
+ new SocketException((int)args.SocketError));
+ }
+ else if (args.BytesTransferred <= 0)
+ {
+ TransmissionResult result = new TransmissionResult(args);
+
+ asyncReceiveFromToken.CompletionSource.SetResult(result);
+ }
+ else
+ {
+ args.MemoryBuffer.CopyTo(asyncReceiveFromToken.UserBuffer);
+
+ TransmissionResult result = new TransmissionResult(args);
+
+ asyncReceiveFromToken.CompletionSource.SetResult(result);
+ }
+ }
+
+ receiveFromBufferPool.Return(asyncReceiveFromToken.RentedBuffer, true);
+ receiveArgsPool.Return(args);
+
+ break;
+
+ default:
+ throw new InvalidOperationException(
+ $"The {nameof(Connection)} class doesn't support the {args.LastOperation} operation.");
+ }
+ }
+
+ private readonly struct AsyncAcceptToken
+ {
+ public readonly CancellationToken CancellationToken;
+ public readonly TaskCompletionSource<SocketAsyncEventArgs> CompletionSource;
+
+ public AsyncAcceptToken(TaskCompletionSource<SocketAsyncEventArgs> tcs, CancellationToken cancellationToken = default)
+ {
+ CompletionSource = tcs;
+ CancellationToken = cancellationToken;
+ }
+ }
+
+ private readonly struct AsyncConnectToken
+ {
+ public readonly CancellationToken CancellationToken;
+ public readonly TaskCompletionSource<SocketAsyncEventArgs> CompletionSource;
+
+ public AsyncConnectToken(TaskCompletionSource<SocketAsyncEventArgs> tcs, CancellationToken cancellationToken = default)
+ {
+ CompletionSource = tcs;
+ CancellationToken = cancellationToken;
+ }
+ }
+
+ private readonly struct AsyncDisconnectToken
+ {
+ public readonly CancellationToken CancellationToken;
+ public readonly TaskCompletionSource<bool> CompletionSource;
+
+ public AsyncDisconnectToken(TaskCompletionSource<bool> tcs, CancellationToken cancellationToken = default)
+ {
+ CompletionSource = tcs;
+ CancellationToken = cancellationToken;
+ }
+ }
+
+ private readonly struct AsyncReadToken
+ {
+ public readonly CancellationToken CancellationToken;
+ public readonly TaskCompletionSource<TransmissionResult> CompletionSource;
+ public readonly byte[] RentedBuffer;
+ public readonly Memory<byte> UserBuffer;
+
+ public AsyncReadToken(byte[] rentedBuffer, Memory<byte> userBuffer, TaskCompletionSource<TransmissionResult> tcs,
+ CancellationToken cancellationToken = default)
+ {
+ RentedBuffer = rentedBuffer;
+ UserBuffer = userBuffer;
+
+ CompletionSource = tcs;
+ CancellationToken = cancellationToken;
+ }
+ }
+
+ private readonly struct AsyncWriteToken
+ {
+ public readonly CancellationToken CancellationToken;
+ public readonly TaskCompletionSource<int> CompletionSource;
+ public readonly byte[] RentedBuffer;
+
+ public AsyncWriteToken(byte[] rentedBuffer, TaskCompletionSource<int> tcs,
+ CancellationToken cancellationToken = default)
+ {
+ RentedBuffer = rentedBuffer;
+
+ CompletionSource = tcs;
+ CancellationToken = cancellationToken;
+ }
+ }
+
+ /// <summary>
+ /// The maximum number of stream connection that will be accepted.
+ /// </summary>
+ /// TODO change this to a configurable builder option
+ public const int MaximumConnectionBacklog = 10;
+
+ /// <summary>
+ /// The maximum number of packets that will be stored before older packets start to be dropped.
+ /// </summary>
+ /// TODO change this to a configurable builder option
+ public const int MaximumPacketBacklog = 64;
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public Task<TransmissionResult> ReceiveAsync(Memory<byte> inputBuffer, SocketFlags flags, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ if (connectionAsyncEventArgs == null)
+ {
+ throw new ConstraintException(
+ $"{nameof(TryConnectAsync)} has not yet been called, or a valid connection has not been made.");
+ }
+
+ return DoReceiveFromAsync(connectionAsyncEventArgs.ConnectSocket, connectionAsyncEventArgs.ConnectSocket.RemoteEndPoint, flags, inputBuffer, cts.Token);
+ }
+
+ public Task<TransmissionResult> ReceiveFromAsync(EndPoint remoteEndPoint, Memory<byte> inputBuffer, SocketFlags flags, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ return DoReceiveFromAsync(datagramSocket, remoteEndPoint, flags, inputBuffer, cts.Token);
+ }
+
+ public ValueTask<int> SendAsync(Memory<byte> outputBuffer, SocketFlags flags, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ if (connectionAsyncEventArgs == null)
+ {
+ throw new ConstraintException(
+ $"{nameof(TryConnectAsync)} has not yet been called, or a valid connection has not been made.");
+ }
+
+ return DoSendToAsync(connectionAsyncEventArgs.ConnectSocket, connectionAsyncEventArgs.ConnectSocket.RemoteEndPoint, flags, outputBuffer, cts.Token);
+ }
+
+ public ValueTask<int> SendToAsync(EndPoint remoteEndPoint, Memory<byte> outputBuffer, SocketFlags flags, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ return DoSendToAsync(datagramSocket, remoteEndPoint, flags, outputBuffer, cts.Token);
+ }
+
+ /// <summary>
+ /// Configures the logger to log messages to the given stream (or to <see cref="Stream.Null"/> if <c>null</c>) and
+ /// to only log messages that are of severity <paramref name="minimumLoggedSeverity"/> or higher.
+ /// </summary>
+ /// <param name="loggingStream">The stream to which messages will be logged.</param>
+ /// <param name="minimumLoggedSeverity">The minimum severity a message must be to be logged.</param>
+ public void SetLoggingStream(Stream? loggingStream, LogLevel minimumLoggedSeverity = LogLevel.Info)
+ {
+ lock (loggerLockObject)
+ {
+ logger = new Logger(loggingStream ?? Stream.Null, minimumLoggedSeverity);
+ }
+ }
+
+ /// <summary>
+ /// Attempts to asynchronously bind the underlying socket to the given local endpoint. Does not block.
+ /// If the timeout is exceeded the binding attempt is aborted and the method returns false.
+ /// </summary>
+ /// <param name="localEndPoint">The local endpoint to bind to.</param>
+ /// <param name="timeout">The timeout within which to attempt the binding.</param>
+ /// <returns>Whether the binding was successful or not.</returns>
+ public async Task<bool> TryBindAsync(EndPoint localEndPoint, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ try
+ {
+ return await Task.Run(() =>
+ {
+ streamSocket.Bind(localEndPoint);
+ datagramSocket.Bind(localEndPoint);
+
+ return true;
+ }, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ return false;
+ }
+ catch (SocketException ex)
+ {
+ logger.LogException($"Socket exception on binding socket to {localEndPoint}:", ex);
+ return false;
+ }
+ }
+
+ public async Task<bool> TryConnectAsync(EndPoint remoteEndPoint, TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ try
+ {
+ connectionAsyncEventArgs = await DoConnectAsync(streamSocket, remoteEndPoint, cts.Token);
+
+ return true;
+ }
+ catch (TaskCanceledException)
+ {
+ return false;
+ }
+ catch (SocketException ex)
+ {
+ logger.LogException($"Socket exception on connecting socket to {remoteEndPoint}:", ex);
+ return false;
+ }
+ }
+
+ public async Task<bool> TryDisconnectAsync(TimeSpan timeout)
+ {
+ using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(timeout);
+ using CancellationTokenSource cts =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, ServerShutdownToken);
+
+ try
+ {
+ if (connectionAsyncEventArgs == null) return false;
+
+ streamSocket.Shutdown(SocketShutdown.Both);
+ streamSocket.Close(1);
+
+ await DoDisconnectAsync(streamSocket, cts.Token);
+
+ return true;
+ }
+ catch (TaskCanceledException)
+ {
+ return false;
+ }
+ catch (SocketException ex)
+ {
+ logger.LogException($"Socket exception on disconnecting socket:", ex);
+ return false;
+ }
+ }
+ }
+}
+\ No newline at end of file
diff --git a/NetSharp/NetSharp/Extensions/ConnectionExtensions.cs b/NetSharp/NetSharp/Extensions/ConnectionExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Threading;
@@ -13,7 +14,7 @@ namespace NetSharp.Extensions
public static class ConnectionExtensions
{
public static Task<TransmissionResult> ReceiveAsync(this Connection instance,
- Memory<byte> inputBuffer, SocketFlags flags)
+ EndPoint remoteEndPoint, Memory<byte> inputBuffer, SocketFlags flags)
=> instance.ReceiveAsync(inputBuffer, flags, Timeout.InfiniteTimeSpan);
public static Task<TransmissionResult> ReceiveFromAsync(this Connection instance,
@@ -21,19 +22,52 @@ namespace NetSharp.Extensions
=> instance.ReceiveFromAsync(remoteEndPoint, inputBuffer, flags, Timeout.InfiniteTimeSpan);
public static ValueTask<int> SendAsync(this Connection instance,
- Memory<byte> outputBuffer, SocketFlags flags)
+ EndPoint remoteEndPoint, Memory<byte> outputBuffer, SocketFlags flags)
=> instance.SendAsync(outputBuffer, flags, Timeout.InfiniteTimeSpan);
public static ValueTask<int> SendToAsync(this Connection instance,
EndPoint remoteEndPoint, Memory<byte> outputBuffer, SocketFlags flags)
=> instance.SendToAsync(remoteEndPoint, outputBuffer, flags, Timeout.InfiniteTimeSpan);
+ /// <summary>
+ /// Attempts to synchronously bind the underlying socket to the given local endpoint. Blocks.
+ /// If the timeout is exceeded the binding attempt is aborted and the method returns false.
+ /// </summary>
+ /// <param name="localEndPoint">The local endpoint to bind to.</param>
+ /// <param name="timeout">The timeout within which to attempt the binding.</param>
+ /// <returns>Whether the binding was successful or not.</returns>
+ public static bool TryBind(this Connection instance,
+ EndPoint localEndPoint, TimeSpan timeout)
+ => instance.TryBindAsync(localEndPoint, timeout).Result;
+
public static bool TryBind(this Connection instance,
EndPoint localEndPoint)
- => instance.TryBind(localEndPoint, Timeout.InfiniteTimeSpan);
+ => instance.TryBindAsync(localEndPoint, Timeout.InfiniteTimeSpan).Result;
public static Task<bool> TryBindAsync(this Connection instance,
EndPoint localEndPoint)
=> instance.TryBindAsync(localEndPoint, Timeout.InfiniteTimeSpan);
+
+ public static bool TryConnect(this Connection instance,
+ EndPoint remoteEndPoint)
+ => instance.TryConnectAsync(remoteEndPoint, Timeout.InfiniteTimeSpan).Result;
+
+ public static bool TryConnect(this Connection instance,
+ EndPoint remoteEndPoint, TimeSpan timeout)
+ => instance.TryConnectAsync(remoteEndPoint, timeout).Result;
+
+ public static Task<bool> TryConnectAsync(this Connection instance,
+ EndPoint remoteEndPoint)
+ => instance.TryConnectAsync(remoteEndPoint, Timeout.InfiniteTimeSpan);
+
+ public static bool TryDisconnect(this Connection instance)
+ => instance.TryDisconnectAsync(Timeout.InfiniteTimeSpan).Result;
+
+ public static bool TryDisconnect(this Connection instance,
+ TimeSpan timeout)
+ => instance.TryDisconnectAsync(timeout).Result;
+
+ public static Task<bool> TryDisconnectAsync(this Connection instance)
+ => instance.TryDisconnectAsync(Timeout.InfiniteTimeSpan);
}
}
\ No newline at end of file
diff --git a/NetSharp/NetSharp/NetSharp.xml b/NetSharp/NetSharp/NetSharp.xml
@@ -8,10 +8,8 @@
<summary>
Encapsulates a connection capable of receiving packets and responding to them with registered packet handlers.
</summary>
- </member>
- <member name="F:NetSharp.Connection.AnyRemoteEndPoint">
<summary>
- Represents any remote endpoint for datagram operations.
+ Implements low-level network access on top of which the rest of the connection is built upon.
</summary>
</member>
<member name="F:NetSharp.Connection.incomingPacketPipeline">
@@ -19,29 +17,52 @@
Pipeline to convert incoming byte buffers to <see cref="T:NetSharp.Packets.NetworkPacket"/> instances.
</summary>
</member>
+ <member name="F:NetSharp.Connection.loggerLockObject">
+ <summary>
+ Lock synchronisation object for the <see cref="F:NetSharp.Connection.logger"/> variable.
+ </summary>
+ </member>
<member name="F:NetSharp.Connection.outgoingPacketPipeline">
<summary>
Pipeline to convert outgoing <see cref="T:NetSharp.Packets.NetworkPacket"/> instances to a byte buffer for sending.
</summary>
</member>
+ <member name="F:NetSharp.Connection.ServerShutdownToken">
+ <summary>
+ Cancellation token which allows observing the shutdown of the server. It is set when <see cref="M:NetSharp.Connection.ShutdownServer"/> is called.
+ </summary>
+ </member>
+ <member name="F:NetSharp.Connection.logger">
+ <summary>
+ A logger object allowing for writing debug messages to an output stream.
+ </summary>
+ </member>
<member name="M:NetSharp.Connection.Finalize">
<summary>
Destroys a <see cref="T:NetSharp.Connection"/> class instance, freeing all managed resources.
</summary>
</member>
- <member name="F:NetSharp.Connection.loggerLockObject">
+ <member name="M:NetSharp.Connection.RunServerAsync">
<summary>
- Lock synchronisation object for the <see cref="F:NetSharp.Connection.logger"/> variable.
+ Makes the connection listen for incoming request packets, and handle them according to registered packet handler delegates.
+ This work can be cancelled by calling <see cref="M:NetSharp.Connection.ShutdownServer"/>.
</summary>
+ <returns>The task representing the connection work.</returns>
</member>
- <member name="F:NetSharp.Connection.ServerShutdownToken">
+ <member name="M:NetSharp.Connection.ShutdownServer">
<summary>
- Cancellation token which allows observing the shutdown of the server. It is set when <see cref="M:NetSharp.Connection.ShutdownServer"/> is called.
+ Shuts down the connection, and releases managed and unmanaged resources.
</summary>
</member>
- <member name="F:NetSharp.Connection.logger">
+ <member name="F:NetSharp.Connection.AnyRemoteEndPoint">
<summary>
- A logger object allowing for writing debug messages to an output stream.
+ Represents any remote endpoint for datagram operations.
+ </summary>
+ </member>
+ <member name="F:NetSharp.Connection.connectionAsyncEventArgs">
+ <summary>
+ Socket async event args object used when the connection instance is used as a client. It is set when a
+ call to <see cref="M:NetSharp.Connection.TryConnectAsync(System.Net.EndPoint,System.TimeSpan)"/> is made.
</summary>
</member>
<member name="M:NetSharp.Connection.Dispose(System.Boolean)">
@@ -50,6 +71,57 @@
</summary>
<param name="disposing">Whether this method is called by <see cref="M:NetSharp.Connection.Dispose"/> or by the finaliser.</param>
</member>
+ <member name="M:NetSharp.Connection.DoAcceptAsync(System.Net.Sockets.Socket,System.Threading.CancellationToken)">
+ <summary>
+ Provides an awaitable wrapper around an asynchronous socket accept operation.
+ </summary>
+ <param name="serverSocket">The socket which should be used to accept an incoming connection attempt.</param>
+ <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ <returns>The accepted socket.</returns>
+ </member>
+ <member name="M:NetSharp.Connection.DoConnectAsync(System.Net.Sockets.Socket,System.Net.EndPoint,System.Threading.CancellationToken)">
+ <summary>
+ Provides an awaitable wrapper around an asynchronous socket connect operation.
+ </summary>
+ <param name="disconnectedSocket">The socket which should asynchronously connect to the remote endpoint.</param>
+ <param name="remoteEndPoint">The remote endpoint to which the socket should connect.</param>
+ <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ </member>
+ <member name="M:NetSharp.Connection.DoDisconnectAsync(System.Net.Sockets.Socket,System.Threading.CancellationToken)">
+ <summary>
+ Provides an awaitable wrapper around an asynchronous socket disconnect operation.
+ </summary>
+ <param name="connectedSocket">The socket which should asynchronously disconnect from its remote endpoint.</param>
+ <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ </member>
+ <member name="M:NetSharp.Connection.DoReceiveFromAsync(System.Net.Sockets.Socket,System.Net.EndPoint,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
+ <summary>
+ Provides an awaitable wrapper around an asynchronous socket receive operation.
+ </summary>
+ <param name="listenerSocket">The socket which should receive data from the remote endpoint.</param>
+ <param name="remoteEndPoint">The remove endpoint from which data should be received.</param>
+ <param name="socketFlags">The socket flags associated with the receive operation.</param>
+ <param name="inputBuffer">The memory buffer into which received data will be stored.</param>
+ <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ <returns>The result of the receive operation from the remote endpoint.</returns>
+ </member>
+ <member name="M:NetSharp.Connection.DoSendToAsync(System.Net.Sockets.Socket,System.Net.EndPoint,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
+ <summary>
+ Provides an awaitable wrapper around an asynchronous socket send operation.
+ </summary>
+ <param name="transmitterSocket">The socket which should send the data to the remote endpoint.</param>
+ <param name="remoteEndPoint">The remote endpoint to which data should be written.</param>
+ <param name="socketFlags">The socket flags associated with the send operation.</param>
+ <param name="outputBuffer">The data buffer which should be sent.</param>
+ <param name="cancellationToken">The cancellation token to observe for the operation.</param>
+ <returns>The result of the send operation to the remote endpoint.</returns>
+ </member>
+ <member name="F:NetSharp.Connection.MaximumConnectionBacklog">
+ <summary>
+ The maximum number of stream connection that will be accepted.
+ </summary>
+ TODO change this to a configurable builder option
+ </member>
<member name="F:NetSharp.Connection.MaximumPacketBacklog">
<summary>
The maximum number of packets that will be stored before older packets start to be dropped.
@@ -59,13 +131,6 @@
<member name="M:NetSharp.Connection.Dispose">
<inheritdoc />
</member>
- <member name="M:NetSharp.Connection.RunServerAsync">
- <summary>
- Makes the connection listen for incoming request packets, and handle them according to registered packet handler delegates.
- This work can be cancelled by calling <see cref="M:NetSharp.Connection.ShutdownServer"/>.
- </summary>
- <returns>The task representing the connection work.</returns>
- </member>
<member name="M:NetSharp.Connection.SetLoggingStream(System.IO.Stream,NetSharp.Logging.LogLevel)">
<summary>
Configures the logger to log messages to the given stream (or to <see cref="F:System.IO.Stream.Null"/> if <c>null</c>) and
@@ -74,20 +139,6 @@
<param name="loggingStream">The stream to which messages will be logged.</param>
<param name="minimumLoggedSeverity">The minimum severity a message must be to be logged.</param>
</member>
- <member name="M:NetSharp.Connection.ShutdownServer">
- <summary>
- Shuts down the connection, and releases managed and unmanaged resources.
- </summary>
- </member>
- <member name="M:NetSharp.Connection.TryBind(System.Net.EndPoint,System.TimeSpan)">
- <summary>
- Attempts to synchronously bind the underlying socket to the given local endpoint. Blocks.
- If the timeout is exceeded the binding attempt is aborted and the method returns false.
- </summary>
- <param name="localEndPoint">The local endpoint to bind to.</param>
- <param name="timeout">The timeout within which to attempt the binding.</param>
- <returns>Whether the binding was successful or not.</returns>
- </member>
<member name="M:NetSharp.Connection.TryBindAsync(System.Net.EndPoint,System.TimeSpan)">
<summary>
Attempts to asynchronously bind the underlying socket to the given local endpoint. Does not block.
@@ -1293,6 +1344,15 @@
Provides additional methods and functionality to the <see cref="T:NetSharp.Connection"/> class.
</summary>
</member>
+ <member name="M:NetSharp.Extensions.ConnectionExtensions.TryBind(NetSharp.Connection,System.Net.EndPoint,System.TimeSpan)">
+ <summary>
+ Attempts to synchronously bind the underlying socket to the given local endpoint. Blocks.
+ If the timeout is exceeded the binding attempt is aborted and the method returns false.
+ </summary>
+ <param name="localEndPoint">The local endpoint to bind to.</param>
+ <param name="timeout">The timeout within which to attempt the binding.</param>
+ <returns>Whether the binding was successful or not.</returns>
+ </member>
<member name="T:NetSharp.Logging.LogLevel">
<summary>
Specifies the severity level of a log message.
@@ -1993,16 +2053,6 @@
Helper class providing awaitable wrappers around asynchronous Receive and ReceiveFrom operations.
</summary>
</member>
- <member name="M:NetSharp.Sockets.SocketReader.ReceiveAsync(System.Net.Sockets.Socket,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
- <summary>
- Provides an awaitable wrapper around an asynchronous socket receive operation.
- </summary>
- <param name="socket">The socket which should receive data from the remote connection.</param>
- <param name="socketFlags">The socket flags associated with the receive operation.</param>
- <param name="inputBuffer">The memory buffer into which received data will be stored.</param>
- <param name="cancellationToken">The cancellation token to observe for the operation.</param>
- <returns>The result of the receive operation.</returns>
- </member>
<member name="M:NetSharp.Sockets.SocketReader.ReceiveFromAsync(System.Net.Sockets.Socket,System.Net.EndPoint,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
<summary>
Provides an awaitable wrapper around an asynchronous socket receive operation.
@@ -2019,16 +2069,6 @@
Helper class providing awaitable wrappers around asynchronous Send and SendTo operations.
</summary>
</member>
- <member name="M:NetSharp.Sockets.SocketWriter.SendAsync(System.Net.Sockets.Socket,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
- <summary>
- Provides an awaitable wrapper around an asynchronous socket send operation.
- </summary>
- <param name="socket">The socket which should send the data to its remote connection.</param>
- <param name="socketFlags">The socket flags associated with the send operation.</param>
- <param name="outputBuffer">The data buffer which should be sent.</param>
- <param name="cancellationToken">The cancellation token to observe for the operation.</param>
- <returns>The number of bytes of data which were written to the remote connection.</returns>
- </member>
<member name="M:NetSharp.Sockets.SocketWriter.SendToAsync(System.Net.Sockets.Socket,System.Net.EndPoint,System.Net.Sockets.SocketFlags,System.Memory{System.Byte},System.Threading.CancellationToken)">
<summary>
Provides an awaitable wrapper around an asynchronous socket send operation.
diff --git a/NetSharp/NetSharp/Sockets/SocketReader.cs b/NetSharp/NetSharp/Sockets/SocketReader.cs
@@ -16,8 +16,6 @@ namespace NetSharp.Sockets
public sealed class SocketReader
{
private readonly int PacketBufferLength;
- private readonly ObjectPool<SocketAsyncEventArgs> receiveAsyncEventArgsPool;
- private readonly ArrayPool<byte> receiveBufferPool;
private readonly ObjectPool<SocketAsyncEventArgs> receiveFromAsyncEventArgsPool;
private readonly ArrayPool<byte> receiveFromBufferPool;
@@ -25,35 +23,6 @@ namespace NetSharp.Sockets
{
switch (args.LastOperation)
{
- case SocketAsyncOperation.Receive:
- AsyncReadToken asyncReceiveToken = (AsyncReadToken)args.UserToken;
-
- if (asyncReceiveToken.CancellationToken.IsCancellationRequested)
- {
- asyncReceiveToken.CompletionSource.SetCanceled();
- }
- else
- {
- if (args.SocketError != SocketError.Success)
- {
- asyncReceiveToken.CompletionSource.SetException(
- new SocketException((int)args.SocketError));
- }
- else
- {
- args.MemoryBuffer.CopyTo(asyncReceiveToken.UserBuffer);
-
- TransmissionResult result = new TransmissionResult(args);
-
- asyncReceiveToken.CompletionSource.SetResult(result);
- }
- }
-
- receiveBufferPool.Return(asyncReceiveToken.RentedBuffer, true);
- receiveAsyncEventArgsPool.Return(args);
-
- break;
-
case SocketAsyncOperation.ReceiveFrom:
AsyncReadToken asyncReceiveFromToken = (AsyncReadToken)args.UserToken;
@@ -112,12 +81,6 @@ namespace NetSharp.Sockets
{
PacketBufferLength = packetBufferLength;
- receiveBufferPool = ArrayPool<byte>.Create(packetBufferLength, maxPooledObjects);
-
- receiveAsyncEventArgsPool =
- new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(),
- maxPooledObjects);
-
receiveFromBufferPool = ArrayPool<byte>.Create(packetBufferLength, maxPooledObjects);
receiveFromAsyncEventArgsPool =
@@ -126,10 +89,6 @@ namespace NetSharp.Sockets
for (int i = 0; i < maxPooledObjects; i++)
{
- SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
- receiveArgs.Completed += HandleIOCompleted;
- receiveAsyncEventArgsPool.Return(receiveArgs);
-
SocketAsyncEventArgs receiveFromArgs = new SocketAsyncEventArgs();
receiveFromArgs.Completed += HandleIOCompleted;
receiveFromAsyncEventArgsPool.Return(receiveFromArgs);
@@ -139,58 +98,6 @@ namespace NetSharp.Sockets
/// <summary>
/// Provides an awaitable wrapper around an asynchronous socket receive operation.
/// </summary>
- /// <param name="socket">The socket which should receive data from the remote connection.</param>
- /// <param name="socketFlags">The socket flags associated with the receive operation.</param>
- /// <param name="inputBuffer">The memory buffer into which received data will be stored.</param>
- /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
- /// <returns>The result of the receive operation.</returns>
- public Task<TransmissionResult> ReceiveAsync(Socket socket, SocketFlags socketFlags,
- Memory<byte> inputBuffer, CancellationToken cancellationToken = default)
- {
- TaskCompletionSource<TransmissionResult> tcs = new TaskCompletionSource<TransmissionResult>();
-
- byte[] rentedReceiveBuffer = receiveBufferPool.Rent(PacketBufferLength);
- Memory<byte> rentedReceiveBufferMemory = new Memory<byte>(rentedReceiveBuffer);
-
- SocketAsyncEventArgs args = receiveAsyncEventArgsPool.Get();
- args.SetBuffer(rentedReceiveBufferMemory);
- args.SocketFlags = socketFlags;
- args.UserToken = new AsyncReadToken(rentedReceiveBuffer, inputBuffer, tcs, cancellationToken);
-
- /*
- // register cleanup action for when the cancellation token is thrown
- cancellationToken.Register(() =>
- {
- tcs.SetCanceled();
-
- receiveBufferPool.Return(rentedReceiveBuffer, true);
-
- //TODO this is probably a hideous solution. find a better one
- args.Completed -= HandleIOCompleted;
- args.Dispose();
-
- SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs();
- newArgs.Completed += HandleIOCompleted;
- receiveAsyncEventArgsPool.Return(newArgs);
- });
- */
-
- // if the receive operation doesn't complete synchronously, returns the awaitable task
- if (socket.ReceiveAsync(args)) return tcs.Task;
-
- args.MemoryBuffer.CopyTo(inputBuffer);
-
- TransmissionResult result = new TransmissionResult(args);
-
- receiveBufferPool.Return(rentedReceiveBuffer, true);
- receiveAsyncEventArgsPool.Return(args);
-
- return Task.FromResult(result);
- }
-
- /// <summary>
- /// Provides an awaitable wrapper around an asynchronous socket receive operation.
- /// </summary>
/// <param name="socket">The socket which should receive data from the remote endpoint.</param>
/// <param name="remoteEndPoint">The remove endpoint from which data should be received.</param>
/// <param name="socketFlags">The socket flags associated with the receive operation.</param>
diff --git a/NetSharp/NetSharp/Sockets/SocketWriter.cs b/NetSharp/NetSharp/Sockets/SocketWriter.cs
@@ -15,8 +15,6 @@ namespace NetSharp.Sockets
public sealed class SocketWriter
{
private readonly int PacketBufferLength;
- private readonly ObjectPool<SocketAsyncEventArgs> sendAsyncEventArgsPool;
- private readonly ArrayPool<byte> sendBufferPool;
private readonly ObjectPool<SocketAsyncEventArgs> sendToAsyncEventArgsPool;
private readonly ArrayPool<byte> sendToBufferPool;
@@ -24,30 +22,6 @@ namespace NetSharp.Sockets
{
switch (args.LastOperation)
{
- case SocketAsyncOperation.Send:
- AsyncWriteToken asyncSendToken = (AsyncWriteToken)args.UserToken;
-
- if (asyncSendToken.CancellationToken.IsCancellationRequested)
- {
- asyncSendToken.CompletionSource.SetCanceled();
- }
- else
- {
- if (args.SocketError != SocketError.Success)
- {
- asyncSendToken.CompletionSource.SetException(
- new SocketException((int)args.SocketError));
- }
- else
- {
- asyncSendToken.CompletionSource.SetResult(args.BytesTransferred);
- }
- }
-
- sendBufferPool.Return(asyncSendToken.RentedBuffer, true);
- sendAsyncEventArgsPool.Return(args);
- break;
-
case SocketAsyncOperation.SendTo:
AsyncWriteToken asyncSendToToken = (AsyncWriteToken)args.UserToken;
@@ -99,12 +73,6 @@ namespace NetSharp.Sockets
{
PacketBufferLength = packetBufferLength;
- sendBufferPool = ArrayPool<byte>.Create(packetBufferLength, maxPooledObjects);
-
- sendAsyncEventArgsPool =
- new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(),
- maxPooledObjects);
-
sendToBufferPool = ArrayPool<byte>.Create(packetBufferLength, maxPooledObjects);
sendToAsyncEventArgsPool =
@@ -113,71 +81,15 @@ namespace NetSharp.Sockets
for (int i = 0; i < maxPooledObjects; i++)
{
- SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
- sendArgs.Completed += HandleIOCompleted;
- sendAsyncEventArgsPool.Return(sendArgs);
-
SocketAsyncEventArgs sendToArgs = new SocketAsyncEventArgs();
sendToArgs.Completed += HandleIOCompleted;
- sendToAsyncEventArgsPool.Return(sendArgs);
+ sendToAsyncEventArgsPool.Return(sendToArgs);
}
}
/// <summary>
/// Provides an awaitable wrapper around an asynchronous socket send operation.
/// </summary>
- /// <param name="socket">The socket which should send the data to its remote connection.</param>
- /// <param name="socketFlags">The socket flags associated with the send operation.</param>
- /// <param name="outputBuffer">The data buffer which should be sent.</param>
- /// <param name="cancellationToken">The cancellation token to observe for the operation.</param>
- /// <returns>The number of bytes of data which were written to the remote connection.</returns>
- public ValueTask<int> SendAsync(Socket socket, SocketFlags socketFlags, Memory<byte> outputBuffer,
- CancellationToken cancellationToken = default)
- {
- TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
-
- byte[] rentedSendBuffer = sendBufferPool.Rent(PacketBufferLength);
- Memory<byte> rentedSendBufferMemory = new Memory<byte>(rentedSendBuffer);
-
- outputBuffer.CopyTo(rentedSendBufferMemory);
-
- SocketAsyncEventArgs args = sendAsyncEventArgsPool.Get();
- args.SetBuffer(rentedSendBufferMemory);
- args.SocketFlags = socketFlags;
- args.UserToken = new AsyncWriteToken(rentedSendBuffer, tcs, cancellationToken);
-
- /*
- // register cleanup action for when the cancellation token is thrown
- cancellationToken.Register(() =>
- {
- tcs.SetCanceled();
-
- sendBufferPool.Return(rentedSendBuffer, true);
-
- //TODO this is probably a hideous solution. find a better one
- args.Completed -= HandleIOCompleted;
- args.Dispose();
-
- SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs();
- newArgs.Completed += HandleIOCompleted;
- sendAsyncEventArgsPool.Return(newArgs);
- });
- */
-
- // if the send operation doesn't complete synchronously, return the awaitable task
- if (socket.SendAsync(args)) return new ValueTask<int>(tcs.Task);
-
- int result = args.BytesTransferred;
-
- sendBufferPool.Return(rentedSendBuffer, true);
- sendAsyncEventArgsPool.Return(args);
-
- return new ValueTask<int>(result);
- }
-
- /// <summary>
- /// Provides an awaitable wrapper around an asynchronous socket send operation.
- /// </summary>
/// <param name="socket">The socket which should send the data to the remote endpoint.</param>
/// <param name="remoteEndPoint">The remote endpoint to which data should be written.</param>
/// <param name="socketFlags">The socket flags associated with the send operation.</param>
diff --git a/NetSharp/NetSharpExamples/Program.cs b/NetSharp/NetSharpExamples/Program.cs
@@ -37,7 +37,7 @@ namespace NetSharpExamples
TimeSpan socketTimeout = TimeSpan.FromSeconds(newtorkTimeout);
const int clientCount = 1;
- const long sentPacketCount = 10_000;
+ const long sentPacketCount = 100;
EndPoint serverEndPoint = new IPEndPoint(serverAddress, serverPort);
ConnectionBuilder clientBuilder = new ConnectionBuilder();
@@ -51,7 +51,8 @@ namespace NetSharpExamples
Console.WriteLine($"Starting client {clientId}");
using Connection client = clientBuilder.Build();
- client.TryBind(new IPEndPoint(IPAddress.Any, 0));
+ await client.TryBindAsync(new IPEndPoint(IPAddress.Any, 0));
+ await client.TryConnectAsync(serverEndPoint);
//client.SetLoggingStream(Console.OpenStandardOutput());
//TimeSpan timeout = TimeSpan.FromMilliseconds(100);
Stopwatch stopwatch = new Stopwatch();
@@ -73,7 +74,7 @@ namespace NetSharpExamples
{
stopwatch.Start();
//int sentBytes = await client.SendToAsync(serverEndPoint, requestPacketBuffer, SocketFlags.None);
- int sentBytes = await client.SendAsync(requestPacketBuffer, SocketFlags.None);
+ int sentBytes = await client.SendAsync(serverEndPoint, requestPacketBuffer, SocketFlags.None);
stopwatch.Stop();
Interlocked.Increment(ref sentPackets);
@@ -81,7 +82,7 @@ namespace NetSharpExamples
stopwatch.Start();
//TransmissionResult result = await client.ReceiveFromAsync(serverEndPoint, responseBuffer, SocketFlags.None);
- TransmissionResult result = await client.ReceiveAsync(responseBuffer, SocketFlags.None);
+ TransmissionResult result = await client.ReceiveAsync(serverEndPoint, responseBuffer, SocketFlags.None);
stopwatch.Stop();
Interlocked.Increment(ref receivedPackets);
@@ -100,6 +101,12 @@ namespace NetSharpExamples
Console.WriteLine($"[Client {clientId}] Sent {sentPacketCount} packets to {serverEndPoint} in {millis} milliseconds");
Console.WriteLine($"[Client {clientId}] Approximate bandwidth: {megabytes / (millis / 1000.0):F3} MBps");
+
+ /*
+ Console.WriteLine($"[Client {clientId}] Closing client...");
+ await client.TryDisconnectAsync();
+ Console.WriteLine($"[Client {clientId}] Closed client.");
+ */
}, i, TaskCreationOptions.LongRunning);
}
@@ -116,32 +123,14 @@ namespace NetSharpExamples
ConnectionBuilder serverBuilder = new ConnectionBuilder();
using Connection server = serverBuilder.WithLogging(Console.OpenStandardOutput(), LogLevel.Info).Build();
- server.TryBind(serverEndPoint);
+ await server.TryBindAsync(serverEndPoint);
//server.SetLoggingStream(Console.OpenStandardOutput());
//server.ChangeLoggingStream(serverOutputStream, LogLevel.Error);
Console.WriteLine("Starting server...");
- EndPoint nullEndPoint = new IPEndPoint(IPAddress.Any, 0);
-
await server.RunServerAsync();
- /*
- byte[] request = new byte[NetworkPacket.PacketSize];
- Memory<byte> requestBuffer = new Memory<byte>(request);
-
- while (true)
- {
- TransmissionResult result = await server.ReceiveFromAsync(nullEndPoint, requestBuffer, SocketFlags.None);
-
- Console.WriteLine($"[Server] Received {result.Count} bytes from {result.RemoteEndPoint}");
-
- int sentBytes = await server.SendToAsync(result.RemoteEndPoint, requestBuffer, SocketFlags.None);
-
- Console.WriteLine($"[Server] Sent {sentBytes} bytes tp {result.RemoteEndPoint}");
- }
- */
-
Console.WriteLine("Server stopped");
Console.ReadLine();