NetSharp

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

commit da9d92cdd92bfe14ac802c9eda7fc32ddca5c364
parent 0793d3083bc8126a125b6162f42020cbcf9e06a4
Author: Mikolaj Lenczewski <33129490+EnderRifter@users.noreply.github.com>
Date:   Thu, 12 Mar 2020 13:07:10 +0000

Went back to basics, with a socket server and socket client.

Diffstat:
MNetSharp/NetSharp/Connection.cs | 74++++++++++++++++++++++++++++++--------------------------------------------
MNetSharp/NetSharp/ConnectionBase.cs | 198++++++++++++++-----------------------------------------------------------------
MNetSharp/NetSharp/Extensions/ConnectionExtensions.cs | 1-
MNetSharp/NetSharp/NetSharp.xml | 50+++++++++++++++++++++++++++++++++++++++++++-------
ANetSharp/NetSharp/Sockets/SocketClient.cs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ANetSharp/NetSharp/Sockets/SocketOperations.cs | 428+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ANetSharp/NetSharp/Sockets/SocketServer.cs | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MNetSharp/NetSharpExamples/Program.cs | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
8 files changed, 905 insertions(+), 240 deletions(-)

diff --git a/NetSharp/NetSharp/Connection.cs b/NetSharp/NetSharp/Connection.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; @@ -21,7 +22,7 @@ namespace NetSharp /// </summary> public sealed partial class Connection : IDisposable { - private readonly ConcurrentDictionary<EndPoint, SocketAsyncEventArgs> datagramConnections; + private readonly HashSet<EndPoint> datagramConnections; private readonly Channel<(EndPoint origin, Memory<byte> packet)> incomingPacketChannel; /// <summary> @@ -48,7 +49,7 @@ namespace NetSharp /// </summary> private readonly CancellationToken ServerShutdownToken; - private readonly ConcurrentDictionary<EndPoint, SocketAsyncEventArgs> streamConnections; + private readonly ConcurrentDictionary<EndPoint, Socket> streamConnections; /// <summary> /// A logger object allowing for writing debug messages to an output stream. @@ -71,8 +72,7 @@ namespace NetSharp async Task StreamListenerWork(object clientArgsObj) { - SocketAsyncEventArgs clientArgs = (SocketAsyncEventArgs)clientArgsObj; - Socket clientSocket = clientArgs.AcceptSocket; + Socket clientSocket = (Socket)clientArgsObj; EndPoint clientEndPoint = clientSocket.RemoteEndPoint; logger.LogMessage($"Client handler started for {clientEndPoint}"); @@ -97,6 +97,8 @@ namespace NetSharp logger.LogMessage($"Client handler stopped for {clientEndPoint}"); + await DoDisconnectAsync(clientSocket, cancellationToken); + clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(1); } @@ -105,15 +107,13 @@ namespace NetSharp while (!cancellationToken.IsCancellationRequested) { - SocketAsyncEventArgs clientArgs = await DoAcceptAsync(streamSocket, cancellationToken); - - Socket clientSocket = clientArgs.AcceptSocket; + Socket clientSocket = await DoAcceptAsync(streamSocket, cancellationToken); if (!streamConnections.ContainsKey(clientSocket.RemoteEndPoint)) { - streamConnections[clientSocket.RemoteEndPoint] = clientArgs; + streamConnections[clientSocket.RemoteEndPoint] = clientSocket; - await Task.Factory.StartNew(StreamListenerWork, clientArgs, ServerShutdownToken); + await Task.Factory.StartNew(StreamListenerWork, streamConnections[clientSocket.RemoteEndPoint], ServerShutdownToken); } else { @@ -132,8 +132,6 @@ 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); @@ -142,14 +140,12 @@ namespace NetSharp await DoReceiveFromAsync(datagramSocket, AnyRemoteEndPoint, SocketFlags.None, receiveBufferMemory, cancellationToken); - if (!datagramConnections.ContainsKey(result.RemoteEndPoint)) + if (!datagramConnections.Contains(result.RemoteEndPoint)) { - datagramConnections[result.RemoteEndPoint] = args; + datagramConnections.Add(result.RemoteEndPoint); } await incomingPacketChannel.Writer.WriteAsync((result.RemoteEndPoint, receiveBufferMemory), cancellationToken); - - clientSocketArgsPool.Return(args); } logger.LogMessage("Stopped datagram listener task."); @@ -190,21 +186,21 @@ namespace NetSharp Memory<byte> serialisedResponse = outgoingPacketPipeline.ProcessPacket(packet); - if (datagramConnections.ContainsKey(destination)) + if (streamConnections.ContainsKey(destination)) { - await DoSendToAsync(datagramSocket, destination, - SocketFlags.None, serialisedResponse, cancellationToken); + Socket streamConnection = streamConnections[destination]; + + await DoSendToAsync(streamConnection, destination, SocketFlags.None, + serialisedResponse, cancellationToken); } - else if (streamConnections.ContainsKey(destination)) + else if (datagramConnections.Contains(destination)) { - SocketAsyncEventArgs streamClientArgs = streamConnections[destination]; - - await DoSendToAsync(streamClientArgs.AcceptSocket, destination, - SocketFlags.None, serialisedResponse, cancellationToken); + await DoSendToAsync(datagramSocket, destination, SocketFlags.None, + serialisedResponse, cancellationToken); } else { - logger.LogWarning($"Dropping packet destined for unknown destination; {destination}"); + logger.LogWarning($"Packet destined for unknown destination: {destination}"); } } @@ -250,12 +246,14 @@ namespace NetSharp new LeakTrackingObjectPool<SocketAsyncEventArgs>( new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), objectPoolSize)); - - 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); + receiveArgsPool = + new LeakTrackingObjectPool<SocketAsyncEventArgs>( + new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), + objectPoolSize)); + sendArgsPool = + new LeakTrackingObjectPool<SocketAsyncEventArgs>( + new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>(), + objectPoolSize)); for (int i = 0; i < objectPoolSize; i++) { @@ -263,18 +261,6 @@ namespace NetSharp clientArgs.Completed += HandleIOCompleted; clientSocketArgsPool.Return(clientArgs); - SocketAsyncEventArgs acceptArgs = new SocketAsyncEventArgs(); - acceptArgs.Completed += HandleIOCompleted; - acceptArgsPool.Return(acceptArgs); - - SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs(); - connectArgs.Completed += HandleIOCompleted; - connectArgsPool.Return(connectArgs); - - SocketAsyncEventArgs disconnectArgs = new SocketAsyncEventArgs(); - disconnectArgs.Completed += HandleIOCompleted; - disconnectArgsPool.Return(disconnectArgs); - SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs(); receiveArgs.Completed += HandleIOCompleted; receiveArgsPool.Return(receiveArgs); @@ -289,8 +275,8 @@ namespace NetSharp //TODO: Preallocate buffers someday } - streamConnections = new ConcurrentDictionary<EndPoint, SocketAsyncEventArgs>(); - datagramConnections = new ConcurrentDictionary<EndPoint, SocketAsyncEventArgs>(); + streamConnections = new ConcurrentDictionary<EndPoint, Socket>(); + datagramConnections = new HashSet<EndPoint>(); this.incomingPacketPipeline = incomingPacketPipeline; BoundedChannelOptions incomingChannelOptions = new BoundedChannelOptions(MaximumPacketBacklog) diff --git a/NetSharp/NetSharp/ConnectionBase.cs b/NetSharp/NetSharp/ConnectionBase.cs @@ -23,16 +23,10 @@ namespace NetSharp /// </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; @@ -46,12 +40,6 @@ namespace NetSharp 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> @@ -73,34 +61,47 @@ namespace NetSharp /// <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) + private async Task<Socket> DoAcceptAsync(Socket serverSocket, CancellationToken cancellationToken = default) { - TaskCompletionSource<SocketAsyncEventArgs> tcs = new TaskCompletionSource<SocketAsyncEventArgs>(); + TaskCompletionSource<Socket> tcs = new TaskCompletionSource<Socket>(); + + cancellationToken.Register(() => tcs.SetCanceled()); - SocketAsyncEventArgs acceptArgs = acceptArgsPool.Get(); - acceptArgs.AcceptSocket = null; - acceptArgs.UserToken = new AsyncAcceptToken(tcs, cancellationToken); + Task<Socket> task = serverSocket.AcceptAsync(); + Task<Socket> completedTask = await Task.WhenAny(task, tcs.Task); - // if the accept operation doesn't complete synchronously, return the awaitable task - return serverSocket.AcceptAsync(acceptArgs) ? tcs.Task : Task.FromResult(acceptArgs); + if (completedTask == task) + { + Socket result = await task; + + tcs.SetResult(result); + } + + return await tcs.Task; } /// <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="socket">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) + private async Task DoConnectAsync(Socket socket, EndPoint remoteEndPoint, CancellationToken cancellationToken = default) { - TaskCompletionSource<SocketAsyncEventArgs> tcs = new TaskCompletionSource<SocketAsyncEventArgs>(); + TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + + cancellationToken.Register(() => tcs.SetCanceled()); - SocketAsyncEventArgs connectArgs = connectArgsPool.Get(); - connectArgs.RemoteEndPoint = remoteEndPoint; - connectArgs.UserToken = new AsyncConnectToken(tcs, cancellationToken); + Task task = socket.ConnectAsync(remoteEndPoint); + Task completedTask = await Task.WhenAny(task, tcs.Task); - // if the connect operation doesn't complete synchronously, return the awaitable task - return disconnectedSocket.ConnectAsync(connectArgs) ? tcs.Task : Task.FromResult(connectArgs); + if (completedTask == task) + { + await task; + tcs.SetResult(true); + } + + await tcs.Task; } /// <summary> @@ -110,21 +111,10 @@ namespace NetSharp /// <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 Task.Factory.StartNew(() => { - return tcs.Task; - } - - disconnectArgsPool.Return(disconnectArgs); - - return Task.CompletedTask; + connectedSocket.Disconnect(true); + }, cancellationToken); } /// <summary> @@ -221,74 +211,6 @@ namespace NetSharp { 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; @@ -355,42 +277,6 @@ namespace NetSharp } } - 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; @@ -450,13 +336,7 @@ namespace NetSharp 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); + return DoReceiveFromAsync(streamSocket, streamSocket.RemoteEndPoint, flags, inputBuffer, cts.Token); } public Task<TransmissionResult> ReceiveFromAsync(EndPoint remoteEndPoint, Memory<byte> inputBuffer, SocketFlags flags, TimeSpan timeout) @@ -474,13 +354,7 @@ namespace NetSharp 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); + return DoSendToAsync(streamSocket, streamSocket.RemoteEndPoint, flags, outputBuffer, cts.Token); } public ValueTask<int> SendToAsync(EndPoint remoteEndPoint, Memory<byte> outputBuffer, SocketFlags flags, TimeSpan timeout) @@ -548,7 +422,7 @@ namespace NetSharp try { - connectionAsyncEventArgs = await DoConnectAsync(streamSocket, remoteEndPoint, cts.Token); + await DoConnectAsync(streamSocket, remoteEndPoint, cts.Token); return true; } @@ -571,13 +445,11 @@ namespace NetSharp try { - if (connectionAsyncEventArgs == null) return false; + await DoDisconnectAsync(streamSocket, cts.Token); streamSocket.Shutdown(SocketShutdown.Both); streamSocket.Close(1); - await DoDisconnectAsync(streamSocket, cts.Token); - return true; } catch (TaskCanceledException) diff --git a/NetSharp/NetSharp/Extensions/ConnectionExtensions.cs b/NetSharp/NetSharp/Extensions/ConnectionExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Net; using System.Net.Sockets; using System.Threading; diff --git a/NetSharp/NetSharp/NetSharp.xml b/NetSharp/NetSharp/NetSharp.xml @@ -59,12 +59,6 @@ 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)"> <summary> Disposes of the managed and unmanaged resources held by this instance. @@ -83,7 +77,7 @@ <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="socket">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> @@ -2048,6 +2042,27 @@ <param name="socket">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.Sockets.SocketClient.Finalize"> + <summary> + Destroys a socket client instance. + </summary> + </member> + <member name="M:NetSharp.Sockets.SocketClient.Dispose(System.Boolean)"> + <summary> + Implementation of dispose pattern. + </summary> + <param name="disposing"> + Whether this method is being called by the object finalizer, or by the <see cref="M:NetSharp.Sockets.SocketClient.Dispose"/> method. + </param> + </member> + <member name="M:NetSharp.Sockets.SocketClient.Dispose"> + <inheritdoc /> + </member> + <member name="T:NetSharp.Sockets.SocketOperations"> + <summary> + Provides helper awaitable functions for wrapping the <see cref="T:System.Net.Sockets.SocketAsyncEventArgs"/> pattern. + </summary> + </member> <member name="T:NetSharp.Sockets.SocketReader"> <summary> Helper class providing awaitable wrappers around asynchronous Receive and ReceiveFrom operations. @@ -2064,6 +2079,27 @@ <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.SocketServer.Finalize"> + <summary> + Destroys a socket server instance. + </summary> + </member> + <member name="F:NetSharp.Sockets.SocketServer.listenerSocket"> + <summary> + The socket which should be used to listen for incoming data and to send outgoing data. + </summary> + </member> + <member name="M:NetSharp.Sockets.SocketServer.Dispose(System.Boolean)"> + <summary> + Implementation of dispose pattern. + </summary> + <param name="disposing"> + Whether this method is being called by the object finalizer, or by the <see cref="M:NetSharp.Sockets.SocketServer.Dispose"/> method. + </param> + </member> + <member name="M:NetSharp.Sockets.SocketServer.Dispose"> + <inheritdoc /> + </member> <member name="T:NetSharp.Sockets.SocketWriter"> <summary> Helper class providing awaitable wrappers around asynchronous Send and SendTo operations. diff --git a/NetSharp/NetSharp/Sockets/SocketClient.cs b/NetSharp/NetSharp/Sockets/SocketClient.cs @@ -0,0 +1,92 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using NetSharp.Utils; + +namespace NetSharp.Sockets +{ + public class SocketClient : IDisposable + { + private readonly ObjectPool<SocketAsyncEventArgs> transmissionArgsPool; + + /// <summary> + /// Destroys a socket client instance. + /// </summary> + ~SocketClient() + { + Dispose(false); + } + + protected readonly Socket transmitterSocket; + + /// <summary> + /// Implementation of dispose pattern. + /// </summary> + /// <param name="disposing"> + /// Whether this method is being called by the object finalizer, or by the <see cref="Dispose()"/> method. + /// </param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + transmitterSocket.Dispose(); + } + } + + public SocketClient(AddressFamily transmitterAddressFamily, SocketType transmitterSocketType, + ProtocolType transmitterProtocolType) + { + transmitterSocket = new Socket(transmitterAddressFamily, transmitterSocketType, transmitterProtocolType); + + transmissionArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>()); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public ValueTask<TransmissionResult> ReceiveAsync(EndPoint remoteEndPoint, SocketFlags receiveFlags, Memory<byte> receiveBuffer, + CancellationToken cancellationToken = default) + { + return SocketOperations.ReceiveFromAsync(transmissionArgsPool, transmitterSocket, remoteEndPoint, + receiveFlags, receiveBuffer, cancellationToken); + } + + public ValueTask<TransmissionResult> SendAsync(EndPoint remoteEndPoint, SocketFlags sendFlags, Memory<byte> sendBuffer, + CancellationToken cancellationToken = default) + { + return SocketOperations.SendToAsync(transmissionArgsPool, transmitterSocket, remoteEndPoint, sendFlags, + sendBuffer, cancellationToken); + } + + public Task<bool> TryBindAsync(EndPoint localEndPoint, TimeSpan timeout) + { + using CancellationTokenSource cts = new CancellationTokenSource(timeout); + + try + { + return Task.Run(() => + { + transmitterSocket.Bind(localEndPoint); + + return true; + }, cts.Token); + } + catch (TaskCanceledException) + { + return Task.FromResult(false); + } + catch (SocketException ex) + { + Console.WriteLine($"Socket exception on binding socket to {localEndPoint}: {ex}"); + return Task.FromResult(false); + } + } + } +} +\ No newline at end of file diff --git a/NetSharp/NetSharp/Sockets/SocketOperations.cs b/NetSharp/NetSharp/Sockets/SocketOperations.cs @@ -0,0 +1,427 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using NetSharp.Utils; + +namespace NetSharp.Sockets +{ + /// <summary> + /// Provides helper awaitable functions for wrapping the <see cref="SocketAsyncEventArgs"/> pattern. + /// </summary> + public static class SocketOperations + { + private static void HandleIOCompleted(object? sender, SocketAsyncEventArgs args) + { + args.Completed -= HandleIOCompleted; + + switch (args.LastOperation) + { + 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 + { + TransmissionResult result = new TransmissionResult(args); + + asyncReceiveFromToken.CompletionSource.SetResult(result); + } + } + + asyncReceiveFromToken.ReadArgsPool.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 + { + TransmissionResult result = new TransmissionResult(args); + + asyncSendToToken.CompletionSource.SetResult(result); + } + } + + asyncSendToToken.WriteArgsPool.Return(args); + break; + + 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.AcceptSocket); + } + } + + asyncAcceptToken.AcceptArgsPool.Return(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.ConnectSocket); + } + } + + asyncConnectToken.ConnectArgsPool.Return(args); + + break; + + case SocketAsyncOperation.Disconnect: + AsyncOperationToken asyncDisconnectToken = (AsyncOperationToken)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); + } + } + + asyncDisconnectToken.OperationArgsPool.Return(args); + + break; + + default: + throw new InvalidOperationException( + $"The {nameof(SocketReader)} class doesn't support the {args.LastOperation} operation."); + } + } + + private readonly struct AsyncAcceptToken + { + public readonly ObjectPool<SocketAsyncEventArgs> AcceptArgsPool; + public readonly CancellationToken CancellationToken; + public readonly TaskCompletionSource<Socket> CompletionSource; + + public AsyncAcceptToken(in ObjectPool<SocketAsyncEventArgs> argsPool, in TaskCompletionSource<Socket> tcs, + in CancellationToken cancellationToken = default) + { + AcceptArgsPool = argsPool; + + CompletionSource = tcs; + CancellationToken = cancellationToken; + } + } + + private readonly struct AsyncConnectToken + { + public readonly CancellationToken CancellationToken; + public readonly TaskCompletionSource<Socket> CompletionSource; + public readonly ObjectPool<SocketAsyncEventArgs> ConnectArgsPool; + + public AsyncConnectToken(in ObjectPool<SocketAsyncEventArgs> argsPool, in TaskCompletionSource<Socket> tcs, + in CancellationToken cancellationToken = default) + { + ConnectArgsPool = argsPool; + + CompletionSource = tcs; + CancellationToken = cancellationToken; + } + } + + private readonly struct AsyncOperationToken + { + public readonly CancellationToken CancellationToken; + public readonly TaskCompletionSource<bool> CompletionSource; + public readonly ObjectPool<SocketAsyncEventArgs> OperationArgsPool; + + public AsyncOperationToken(in ObjectPool<SocketAsyncEventArgs> argsPool, in TaskCompletionSource<bool> tcs, + in CancellationToken cancellationToken = default) + { + OperationArgsPool = argsPool; + + CompletionSource = tcs; + CancellationToken = cancellationToken; + } + } + + private readonly struct AsyncReadToken + { + public readonly CancellationToken CancellationToken; + public readonly TaskCompletionSource<TransmissionResult> CompletionSource; + public readonly ObjectPool<SocketAsyncEventArgs> ReadArgsPool; + public readonly Memory<byte> UserBuffer; + + public AsyncReadToken(in ObjectPool<SocketAsyncEventArgs> argsPool, in Memory<byte> userBuffer, + in TaskCompletionSource<TransmissionResult> tcs, in CancellationToken cancellationToken = default) + { + ReadArgsPool = argsPool; + UserBuffer = userBuffer; + + CompletionSource = tcs; + CancellationToken = cancellationToken; + } + } + + private readonly struct AsyncWriteToken + { + public readonly CancellationToken CancellationToken; + public readonly TaskCompletionSource<TransmissionResult> CompletionSource; + public readonly ObjectPool<SocketAsyncEventArgs> WriteArgsPool; + + public AsyncWriteToken(in ObjectPool<SocketAsyncEventArgs> argsPool, + in TaskCompletionSource<TransmissionResult> tcs, in CancellationToken cancellationToken = default) + { + WriteArgsPool = argsPool; + + CompletionSource = tcs; + CancellationToken = cancellationToken; + } + } + + public static Task<Socket> AcceptAsync(ObjectPool<SocketAsyncEventArgs> acceptArgsPool, + Socket socket, CancellationToken cancellationToken = default) + { + TaskCompletionSource<Socket> tcs = new TaskCompletionSource<Socket>(); + + SocketAsyncEventArgs args = acceptArgsPool.Get(); + args.UserToken = new AsyncAcceptToken(acceptArgsPool, tcs, cancellationToken); + + /* + // register cleanup action for when the cancellation token is thrown + cancellationToken.Register(() => + { + tcs.SetCanceled(); + + //TODO this is probably a hideous solution. find a better one + args.Completed -= HandleIOCompleted; + args.Dispose(); + + SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs(); + newArgs.Completed += HandleIOCompleted; + acceptAsyncEventArgsPool.Return(newArgs); + }); + */ + + // if the accept operation doesn't complete synchronously, return the awaitable task + if (socket.AcceptAsync(args)) return tcs.Task; + + Socket result = args.AcceptSocket; + args.Completed -= HandleIOCompleted; + + acceptArgsPool.Return(args); + + return Task.FromResult(result); + } + + public static Task<Socket> ConnectAsync(ObjectPool<SocketAsyncEventArgs> connectArgsPool, + Socket socket, EndPoint remoteEndPoint, CancellationToken cancellationToken = default) + { + TaskCompletionSource<Socket> tcs = new TaskCompletionSource<Socket>(); + + SocketAsyncEventArgs args = connectArgsPool.Get(); + args.RemoteEndPoint = remoteEndPoint; + args.UserToken = new AsyncConnectToken(connectArgsPool, tcs, cancellationToken); + + /* + // register cleanup action for when the cancellation token is thrown + cancellationToken.Register(() => + { + tcs.SetCanceled(); + + //TODO this is probably a hideous solution. find a better one + args.Completed -= HandleIOCompleted; + args.Dispose(); + + SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs(); + newArgs.Completed += HandleIOCompleted; + connectAsyncEventArgsPool.Return(newArgs); + }); + */ + + // if the connect operation doesn't complete synchronously, return the awaitable task + if (socket.ConnectAsync(args)) return tcs.Task; + + Socket result = args.ConnectSocket; + args.Completed -= HandleIOCompleted; + + connectArgsPool.Return(args); + + return Task.FromResult(result); + } + + public static Task DisconnectAsync(ObjectPool<SocketAsyncEventArgs> disconnectArgsPool, + Socket socket, CancellationToken cancellationToken = default) + { + TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + + SocketAsyncEventArgs args = disconnectArgsPool.Get(); + args.UserToken = new AsyncOperationToken(disconnectArgsPool, tcs, cancellationToken); + + /* + // register cleanup action for when the cancellation token is thrown + cancellationToken.Register(() => + { + tcs.SetCanceled(); + + //TODO this is probably a hideous solution. find a better one + args.Completed -= HandleIOCompleted; + args.Dispose(); + + SocketAsyncEventArgs newArgs = new SocketAsyncEventArgs(); + newArgs.Completed += HandleIOCompleted; + disconnectAsyncEventArgsPool.Return(newArgs); + }); + */ + + // if the disconnect operation doesn't complete synchronously, return the awaitable task + if (socket.DisconnectAsync(args)) return tcs.Task; + + args.Completed -= HandleIOCompleted; + + disconnectArgsPool.Return(args); + + return Task.CompletedTask; + } + + public static ValueTask<TransmissionResult> ReceiveFromAsync(ObjectPool<SocketAsyncEventArgs> receiveArgsPool, + Socket socket, EndPoint remoteEndPoint, SocketFlags socketFlags, Memory<byte> inputBuffer, + CancellationToken cancellationToken = default) + { + TaskCompletionSource<TransmissionResult> tcs = new TaskCompletionSource<TransmissionResult>(); + + SocketAsyncEventArgs args = receiveArgsPool.Get(); + args.SetBuffer(inputBuffer); + args.SocketFlags = socketFlags; + args.RemoteEndPoint = remoteEndPoint; + args.UserToken = new AsyncReadToken(receiveArgsPool, inputBuffer, tcs, cancellationToken); + + args.Completed += HandleIOCompleted; + + /* + // register cleanup action for when the cancellation token is thrown + cancellationToken.Register(() => + { + tcs.SetCanceled(); + + receiveBufferPool.Return(rentedReceiveFromBuffer, 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.ReceiveFromAsync(args)) return new ValueTask<TransmissionResult>(tcs.Task); + + args.Completed -= HandleIOCompleted; + + TransmissionResult result = new TransmissionResult(args); + + receiveArgsPool.Return(args); + + return new ValueTask<TransmissionResult>(result); + } + + public static ValueTask<TransmissionResult> SendToAsync(ObjectPool<SocketAsyncEventArgs> sendArgsPool, + Socket socket, EndPoint remoteEndPoint, SocketFlags socketFlags, Memory<byte> outputBuffer, + CancellationToken cancellationToken = default) + { + TaskCompletionSource<TransmissionResult> tcs = new TaskCompletionSource<TransmissionResult>(); + + SocketAsyncEventArgs args = sendArgsPool.Get(); + args.SetBuffer(outputBuffer); + args.SocketFlags = socketFlags; + args.RemoteEndPoint = remoteEndPoint; + args.UserToken = new AsyncWriteToken(sendArgsPool, tcs, cancellationToken); + + args.Completed += HandleIOCompleted; + + /* + // 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 (socket.SendToAsync(args)) return new ValueTask<TransmissionResult>(tcs.Task); + + args.Completed -= HandleIOCompleted; + + TransmissionResult result = new TransmissionResult(args); + + sendArgsPool.Return(args); + + return new ValueTask<TransmissionResult>(result); + } + } +} +\ No newline at end of file diff --git a/NetSharp/NetSharp/Sockets/SocketServer.cs b/NetSharp/NetSharp/Sockets/SocketServer.cs @@ -0,0 +1,95 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using NetSharp.Utils; + +namespace NetSharp.Sockets +{ + public class SocketServer : IDisposable + { + private readonly ObjectPool<SocketAsyncEventArgs> transmissionArgsPool; + + /// <summary> + /// Destroys a socket server instance. + /// </summary> + ~SocketServer() + { + Dispose(false); + } + + /// <summary> + /// The socket which should be used to listen for incoming data and to send outgoing data. + /// </summary> + protected readonly Socket listenerSocket; + + /// <summary> + /// Implementation of dispose pattern. + /// </summary> + /// <param name="disposing"> + /// Whether this method is being called by the object finalizer, or by the <see cref="Dispose()"/> method. + /// </param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + listenerSocket.Dispose(); + } + } + + public SocketServer(AddressFamily listenerAddressFamily, SocketType listenerSocketType, + ProtocolType listenerProtocolType) + { + listenerSocket = new Socket(listenerAddressFamily, listenerSocketType, listenerProtocolType); + + transmissionArgsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledObjectPolicy<SocketAsyncEventArgs>()); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public ValueTask<TransmissionResult> ReceiveAsync(EndPoint remoteEndPoint, SocketFlags receiveFlags, Memory<byte> receiveBuffer, + CancellationToken cancellationToken = default) + { + return SocketOperations.ReceiveFromAsync(transmissionArgsPool, listenerSocket, + remoteEndPoint, receiveFlags, receiveBuffer, cancellationToken); + } + + public ValueTask<TransmissionResult> SendAsync(EndPoint remoteEndPoint, SocketFlags sendFlags, Memory<byte> sendBuffer, + CancellationToken cancellationToken = default) + { + return SocketOperations.SendToAsync(transmissionArgsPool, listenerSocket, + remoteEndPoint, sendFlags, sendBuffer, cancellationToken); + } + + public Task<bool> TryBindAsync(EndPoint localEndPoint, TimeSpan timeout) + { + using CancellationTokenSource cts = new CancellationTokenSource(timeout); + + try + { + return Task.Run(() => + { + listenerSocket.Bind(localEndPoint); + + return true; + }, cts.Token); + } + catch (TaskCanceledException) + { + return Task.FromResult(false); + } + catch (SocketException ex) + { + Console.WriteLine($"Socket exception on binding socket to {localEndPoint}: {ex}"); + return Task.FromResult(false); + } + } + } +} +\ No newline at end of file diff --git a/NetSharp/NetSharpExamples/Program.cs b/NetSharp/NetSharpExamples/Program.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; @@ -10,36 +12,40 @@ using NetSharp; using NetSharp.Extensions; using NetSharp.Logging; using NetSharp.Packets; +using NetSharp.Sockets; using NetSharp.Utils; namespace NetSharpExamples { internal class Program { - private static int newtorkTimeout = 1_000_000; - private static IPAddress serverAddress; - - private static int serverPort; + private const int NetworkTimeout = 1_000_000; + private const int ServerPort = 12374; + private static readonly IPAddress ServerAddress = IPAddress.Parse("10.4.3.167"); // IPAddress.Parse("192.168.0.31"); + private static readonly EndPoint ServerEndPoint = new IPEndPoint(ServerAddress, ServerPort); private static async Task Main() { Console.WriteLine("Hello World!"); - serverAddress = IPAddress.Loopback; - serverPort = 12374; + await Task.Factory.StartNew(TestSocketServer); + await Task.Factory.StartNew(TestSocketClient).Result; + + Console.ReadLine(); - await Task.Factory.StartNew(TestServer); - await Task.Factory.StartNew(TestClient).Result; + //await Task.Factory.StartNew(TestServer); + //await Task.Factory.StartNew(TestClient).Result; } + #region Connection Tests + private static async Task TestClient() { - TimeSpan socketTimeout = TimeSpan.FromSeconds(newtorkTimeout); + TimeSpan socketTimeout = TimeSpan.FromSeconds(NetworkTimeout); - const int clientCount = 1; - const long sentPacketCount = 100; + const int clientCount = 10; + const long sentPacketCount = 10_000; - EndPoint serverEndPoint = new IPEndPoint(serverAddress, serverPort); ConnectionBuilder clientBuilder = new ConnectionBuilder(); Console.WriteLine($"Testing client connections..."); @@ -52,7 +58,7 @@ namespace NetSharpExamples using Connection client = clientBuilder.Build(); await client.TryBindAsync(new IPEndPoint(IPAddress.Any, 0)); - await client.TryConnectAsync(serverEndPoint); + await client.TryConnectAsync(ServerEndPoint); //client.SetLoggingStream(Console.OpenStandardOutput()); //TimeSpan timeout = TimeSpan.FromMilliseconds(100); Stopwatch stopwatch = new Stopwatch(); @@ -74,21 +80,19 @@ namespace NetSharpExamples { stopwatch.Start(); //int sentBytes = await client.SendToAsync(serverEndPoint, requestPacketBuffer, SocketFlags.None); - int sentBytes = await client.SendAsync(serverEndPoint, requestPacketBuffer, SocketFlags.None); + int sentBytes = await client.SendAsync(ServerEndPoint, requestPacketBuffer, SocketFlags.None); stopwatch.Stop(); Interlocked.Increment(ref sentPackets); - Console.WriteLine($"[Client {clientId}] Sent {sentBytes} bytes to {serverEndPoint}"); + //Console.WriteLine($"[Client {clientId}] Sent {sentBytes} bytes to {serverEndPoint}"); stopwatch.Start(); //TransmissionResult result = await client.ReceiveFromAsync(serverEndPoint, responseBuffer, SocketFlags.None); - TransmissionResult result = await client.ReceiveAsync(serverEndPoint, responseBuffer, SocketFlags.None); + TransmissionResult result = await client.ReceiveAsync(ServerEndPoint, responseBuffer, SocketFlags.None); stopwatch.Stop(); Interlocked.Increment(ref receivedPackets); - Console.WriteLine($"[Client {clientId}] Received {result.Count} bytes from {result.RemoteEndPoint}"); - - //await Task.Delay(10); + //Console.WriteLine($"[Client {clientId}] Received {result.Count} bytes from {result.RemoteEndPoint}"); } catch (Exception ex) { @@ -99,14 +103,11 @@ namespace NetSharpExamples long millis = stopwatch.ElapsedMilliseconds; double megabytes = sentPackets * NetworkPacket.DataSegmentSize / 1_000_000.0; - Console.WriteLine($"[Client {clientId}] Sent {sentPacketCount} packets to {serverEndPoint} in {millis} milliseconds"); + 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); } @@ -119,11 +120,10 @@ namespace NetSharpExamples File.Delete(serverLogFile); await using Stream serverOutputStream = File.OpenWrite(serverLogFile); - EndPoint serverEndPoint = new IPEndPoint(serverAddress, serverPort); ConnectionBuilder serverBuilder = new ConnectionBuilder(); using Connection server = serverBuilder.WithLogging(Console.OpenStandardOutput(), LogLevel.Info).Build(); - await server.TryBindAsync(serverEndPoint); + await server.TryBindAsync(ServerEndPoint); //server.SetLoggingStream(Console.OpenStandardOutput()); //server.ChangeLoggingStream(serverOutputStream, LogLevel.Error); @@ -135,5 +135,160 @@ namespace NetSharpExamples Console.ReadLine(); } + + #endregion Connection Tests + + #region Socket Tests + + private static async Task TestSocketClient() + { + const int clientCount = 100; + const long packetsToSend = 100_000; + + Thread[] clientThreads = new Thread[clientCount]; + double[] clientBandwidths = new double[clientCount]; + HashSet<int> activeThreads = new HashSet<int>(clientCount); + + async void ClientTask(object id) + { + try + { + lock (typeof(Console)) + { + Console.WriteLine($"[Client {id}] Starting client..."); + } + + using SocketClient client = new SocketClient(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + if (!await client.TryBindAsync(new IPEndPoint(IPAddress.Any, 0), TimeSpan.FromMilliseconds(1_000))) + { + lock (typeof(Console)) + { + Console.WriteLine($"[Client {id}] Could not bind client socket within timeout!"); + return; + } + } + + byte[] requestBuffer = new byte[NetworkPacket.PacketSize]; + Memory<byte> requestBufferMemory = new Memory<byte>(requestBuffer); + + byte[] responseBuffer = new byte[NetworkPacket.PacketSize]; + Memory<byte> responseBufferMemory = new Memory<byte>(responseBuffer); + + Stopwatch rttStopwatch = new Stopwatch(); + Stopwatch bandwidthStopwatch = new Stopwatch(); + + for (int i = 0; i < packetsToSend; i++) + { + Encoding.UTF8.GetBytes($"Hello World! (Packet {i})").CopyTo(requestBufferMemory); + + rttStopwatch.Start(); + bandwidthStopwatch.Start(); + TransmissionResult sendResult = await client.SendAsync(ServerEndPoint, SocketFlags.None, requestBufferMemory); + bandwidthStopwatch.Stop(); + rttStopwatch.Stop(); + +#if DEBUG + lock (typeof(Console)) + { + //Console.WriteLine($"[Client {id}, Packet {i}] Sent {sendResult.Count} bytes to {sendResult.RemoteEndPoint}"); + //Console.WriteLine($"[Client {id}, Packet {i}] >>>> {Encoding.UTF8.GetString(sendResult.Buffer.Span)}"); + } +#endif + + rttStopwatch.Start(); + bandwidthStopwatch.Start(); + TransmissionResult receiveResult = await client.ReceiveAsync(ServerEndPoint, SocketFlags.None, responseBufferMemory); + bandwidthStopwatch.Stop(); + rttStopwatch.Stop(); + +#if DEBUG + lock (typeof(Console)) + { + //Console.WriteLine($"[Client {id}, Packet {i}] Received {receiveResult.Count} bytes from {receiveResult.RemoteEndPoint}"); + //Console.WriteLine($"[Client {id}, Packet {i}] <<<< {Encoding.UTF8.GetString(receiveResult.Buffer.Span)}"); + } +#endif + + lock (typeof(Console)) + { + if (!activeThreads.Contains((int)id)) + { + activeThreads.Add((int)id); + } + + //Console.WriteLine($"[Client {id}] Client Round Trip Time: {rttStopwatch.ElapsedMilliseconds} ms"); + rttStopwatch.Reset(); + } + } + + long millis = bandwidthStopwatch.ElapsedMilliseconds; + double megabytes = packetsToSend * NetworkPacket.DataSegmentSize / 1_000_000.0; + double bandwidth = megabytes / (millis / 1000.0); + + clientBandwidths[(int)id] = bandwidth; + + lock (typeof(Console)) + { + Console.WriteLine($"[Client {id}] Sent {packetsToSend} packets to {ServerEndPoint} in {millis} milliseconds"); + Console.WriteLine($"[Client {id}] Approximate bandwidth: {bandwidth:F3} MBps"); + + Console.WriteLine($"[Client {id}] Stopping client..."); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + } + + for (int clientId = 0; clientId < clientCount; clientId++) + { + clientThreads[clientId] = new Thread(ClientTask) { Name = $"Client thread {clientId:D3}" }; + clientThreads[clientId].Start(clientId); + } + + Console.ReadLine(); + + int totalActiveThreads = 0; + for (int i = 0; i < clientCount; i++) + { + Thread clientThread = clientThreads[i]; + + bool threadWasActive = activeThreads.Contains(i); + if (threadWasActive) + { + totalActiveThreads++; + } + + Console.WriteLine($"{clientThread.Name} bandwidth: {clientBandwidths[i]}, was active? {threadWasActive}"); + } + + Console.WriteLine($"Total server bandwidth: {clientBandwidths.Sum():F3} MBps, Total Active Threads: {totalActiveThreads}, Total Inactive Threads: {clientCount - totalActiveThreads}"); + } + + private static async Task TestSocketServer() + { + using SocketServer server = + new SocketServer(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + await server.TryBindAsync(ServerEndPoint, TimeSpan.FromMilliseconds(1_000)); + + byte[] requestBuffer = new byte[NetworkPacket.PacketSize]; + Memory<byte> requestBufferMemory = new Memory<byte>(requestBuffer); + + while (true) + { + EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + + TransmissionResult receiveResult = + await server.ReceiveAsync(remoteEndPoint, SocketFlags.None, requestBufferMemory); + + TransmissionResult sendResult = + await server.SendAsync(receiveResult.RemoteEndPoint, SocketFlags.None, requestBuffer); + } + } + + #endregion Socket Tests } } \ No newline at end of file