NetSharp

NetSharp.git
git clone git://git.lenczewski.org/NetSharp.git
Log | Files | Refs | README | LICENSE

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:
MNetSharp/NetSharp/Connection.cs | 295++++++++++++++++++++++++++++---------------------------------------------------
ANetSharp/NetSharp/ConnectionBase.cs | 595+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MNetSharp/NetSharp/Extensions/ConnectionExtensions.cs | 40+++++++++++++++++++++++++++++++++++++---
MNetSharp/NetSharp/NetSharp.xml | 140+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
MNetSharp/NetSharp/Sockets/SocketReader.cs | 93-------------------------------------------------------------------------------
MNetSharp/NetSharp/Sockets/SocketWriter.cs | 90+------------------------------------------------------------------------------
MNetSharp/NetSharpExamples/Program.cs | 35++++++++++++-----------------------
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();