NetSharp

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

commit 2de9d320ce5082fbf9d2d8cd05b4548dceff3554
parent ab046ff034d626e0a975ab22bdd335e249a8548a
Author: Mikołaj Lenczewski <mikolaj.lenczewski308@gmail.com>
Date:   Thu,  4 Mar 2021 23:35:49 +0000

Started another refactor of project

+ moved common dotMemory operations and constants to new TestUtils class
~ split RawStreamConnection into discrete server and client classes, to
better reflect their separate operating modes (server: async
non-blocking, client: (a)sync (non-)blocking)
~ reorganised namespaces, putting some common RawStream types under
Raw.Stream namespace
~ extended IRawStreamClient interface to include ReceiveAsync method,
and added custom return type for SendAsync and said method

Diffstat:
DNetSharp/NetSharp.Benchmarks/Benchmarks/RawStreamConnectionBenchmark.cs | 16----------------
MNetSharp/NetSharp.Benchmarks/NetSharp.Benchmarks.csproj | 4++++
DNetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamClient.cs | 47-----------------------------------------------
DNetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamExample.cs | 60------------------------------------------------------------
DNetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamServer.cs | 47-----------------------------------------------
MNetSharp/NetSharp.Examples/NetSharp.Examples.csproj | 4++++
MNetSharp/NetSharp.Tests/RawDatagramConnectionTests.cs | 16++--------------
MNetSharp/NetSharp.Tests/RawStreamConnectionTests.cs | 58++++++++++++++++------------------------------------------
ANetSharp/NetSharp.Tests/TestUtils.cs | 20++++++++++++++++++++
MNetSharp/NetSharp/NetSharp.xml | 352+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
MNetSharp/NetSharp/Raw/Datagram/RawDatagramConnection.cs | 15+++++++++++++--
ANetSharp/NetSharp/Raw/IRawServer.cs | 14++++++++++++++
MNetSharp/NetSharp/Raw/RawConnectionBase.cs | 56+++++++++++++++++---------------------------------------
DNetSharp/NetSharp/Raw/RawPacket.cs | 30------------------------------
DNetSharp/NetSharp/Raw/RawPacketHeader.cs | 115-------------------------------------------------------------------------------
ANetSharp/NetSharp/Raw/Stream/IRawStreamClient.cs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MNetSharp/NetSharp/Raw/Stream/IRawStreamPacketHandler.cs | 30+++++++++++++++++++++++++++---
DNetSharp/NetSharp/Raw/Stream/IRawStreamWriter.cs | 29-----------------------------
ANetSharp/NetSharp/Raw/Stream/RawPacket.cs | 30++++++++++++++++++++++++++++++
ANetSharp/NetSharp/Raw/Stream/RawPacketHeader.cs | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ANetSharp/NetSharp/Raw/Stream/RawStreamClient.cs | 551+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DNetSharp/NetSharp/Raw/Stream/RawStreamConnection.cs | 931-------------------------------------------------------------------------------
ANetSharp/NetSharp/Raw/Stream/RawStreamServer.cs | 604++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
23 files changed, 1705 insertions(+), 1506 deletions(-)

diff --git a/NetSharp/NetSharp.Benchmarks/Benchmarks/RawStreamConnectionBenchmark.cs b/NetSharp/NetSharp.Benchmarks/Benchmarks/RawStreamConnectionBenchmark.cs @@ -1,16 +0,0 @@ -using System.Threading.Tasks; - -namespace NetSharp.Benchmarks.Benchmarks -{ - public class RawStreamConnectionBenchmark : INetSharpBenchmark - { - /// <inheritdoc /> - public string Name { get; } = "Raw Stream Connection Benchmark"; - - /// <inheritdoc /> - public Task RunAsync() - { - return Task.CompletedTask; - } - } -} diff --git a/NetSharp/NetSharp.Benchmarks/NetSharp.Benchmarks.csproj b/NetSharp/NetSharp.Benchmarks/NetSharp.Benchmarks.csproj @@ -9,4 +9,8 @@ <ProjectReference Include="..\NetSharp\NetSharp.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Benchmarks\" /> + </ItemGroup> + </Project> diff --git a/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamClient.cs b/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamClient.cs @@ -1,47 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -using NetSharp.Raw.Stream; - -namespace NetSharp.Examples.Examples.Raw_Stream_Examples -{ - public class RawStreamClient : INetSharpExample - { - /// <inheritdoc /> - public string Name { get; } = "Raw Stream Client"; - - /// <inheritdoc /> - public Task RunAsync() - { - using RawStreamConnection client = new RawStreamConnection(ProtocolType.Tcp, Constants.DefaultEndPoint); - client.Bind(Constants.ClientEndPoint); - Console.WriteLine($"[Client] Bound to {client.LocalEndPoint}"); - - client.ConnectAsync(Constants.ServerEndPoint).GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Connected to {client.RemoteEndPoint}"); - - byte[] packet = new byte[Constants.PacketSize]; - byte[] message = Constants.ServerEncoding.GetBytes("Hello World!"); - - int sentBytes; - do - { - message.CopyTo(packet, 0); - - sentBytes = client.SendAsync(0, packet).GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Sent {sentBytes} bytes to {client.RemoteEndPoint}"); - - Thread.Sleep(1000); - } while (sentBytes > 0); - - client.DisconnectAsync().GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Disconnected from {client.RemoteEndPoint}"); - - client.Close(); - - return Task.CompletedTask; - } - } -} diff --git a/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamExample.cs b/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamExample.cs @@ -1,60 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; - -using NetSharp.Raw; -using NetSharp.Raw.Stream; - -namespace NetSharp.Examples.Examples.Raw_Stream_Examples -{ - public class RawStreamExample : INetSharpExample - { - /// <inheritdoc /> - public string Name { get; } = "Raw Stream Example"; - - /// <inheritdoc /> - public Task RunAsync() - { - using RawStreamConnection server = new RawStreamConnection(ProtocolType.Tcp, Constants.DefaultEndPoint); - server.Bind(Constants.ServerEndPoint); - Console.WriteLine($"[Server] Bound to {server.LocalEndPoint}"); - - static void PacketHandler( - EndPoint remoteEndPoint, - in RawPacketHeader header, - in ReadOnlyMemory<byte> data, - IRawStreamWriter writer) - { - Console.WriteLine( - $"[Server] Received {header.DataLength} bytes with type {header.Type} from {remoteEndPoint}"); - } - - server.RegisterHandler(0, PacketHandler); - server.Start(); - - Console.WriteLine("[Server] Started listening for network connections..."); - - using RawStreamConnection client = new RawStreamConnection(ProtocolType.Tcp, Constants.DefaultEndPoint); - client.Bind(Constants.ClientEndPoint); - Console.WriteLine($"[Client] Bound to {client.LocalEndPoint}"); - - client.ConnectAsync(Constants.ServerEndPoint).GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Connected to {client.RemoteEndPoint}"); - - byte[] packet = { 42 }; - int sentBytes = client.SendAsync(0, packet).GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Sent {sentBytes} bytes to {client.RemoteEndPoint}"); - - client.DisconnectAsync().GetAwaiter().GetResult(); - Console.WriteLine($"[Client] Disconnected from {client.RemoteEndPoint}"); - - client.Close(); - - server.DeregisterHandler(0, PacketHandler); - server.Close(); - - return Task.CompletedTask; - } - } -} diff --git a/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamServer.cs b/NetSharp/NetSharp.Examples/Examples/Raw Stream Examples/RawStreamServer.cs @@ -1,47 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; - -using NetSharp.Raw; -using NetSharp.Raw.Stream; - -namespace NetSharp.Examples.Examples.Raw_Stream_Examples -{ - public class RawStreamServer : INetSharpExample - { - /// <inheritdoc /> - public string Name { get; } = "Raw Stream Server"; - - /// <inheritdoc /> - public Task RunAsync() - { - using RawStreamConnection server = new RawStreamConnection(ProtocolType.Tcp, Constants.DefaultEndPoint); - server.Bind(Constants.ServerEndPoint); - Console.WriteLine($"[Server] Bound to {server.LocalEndPoint}"); - - static void PacketHandler( - EndPoint remoteEndPoint, - in RawPacketHeader header, - in ReadOnlyMemory<byte> data, - IRawStreamWriter writer) - { - Console.WriteLine( - $"[Server] Received {header.DataLength} bytes with type {header.Type} from {remoteEndPoint}"); - } - - server.RegisterHandler(0, PacketHandler); - server.Start(); - - Console.WriteLine("[Server] Started listening for network connections!"); - Console.WriteLine("[Server] Press enter to stop the server..."); - Console.ReadLine(); - - Console.WriteLine("[Server] Attempting to stop the server..."); - server.Close(); - Console.WriteLine("[Server] Successfully shut down the server!"); - - return Task.CompletedTask; - } - } -} diff --git a/NetSharp/NetSharp.Examples/NetSharp.Examples.csproj b/NetSharp/NetSharp.Examples/NetSharp.Examples.csproj @@ -9,4 +9,8 @@ <ProjectReference Include="..\NetSharp\NetSharp.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Examples\" /> + </ItemGroup> + </Project> diff --git a/NetSharp/NetSharp.Tests/RawDatagramConnectionTests.cs b/NetSharp/NetSharp.Tests/RawDatagramConnectionTests.cs @@ -11,9 +11,6 @@ namespace NetSharp.Tests { public class RawDatagramConnectionTests { - private static readonly IPEndPoint ClientLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - private static readonly IPEndPoint ServerLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 12345); - [Fact] public void DisposesCleanly() { @@ -21,23 +18,14 @@ namespace NetSharp.Tests { using RawDatagramConnection conn = ConnectionFactory(); - conn.Bind(ClientLocalEndPoint); - conn.Start(); + conn.Bind(TestUtils.ClientLocalEndPoint); conn.Close(); } Instantiate(); - AssertDisposedCleanly(); - } - - private static void AssertDisposedCleanly() - { - _ = dotMemory.Check(memory => - { - Assert.Equal(0, memory.GetObjects(where => where.Type.Is<RawDatagramConnection>()).ObjectsCount); - }); + TestUtils.AssertDisposedCleanly<RawDatagramConnection>(); } private static RawDatagramConnection ConnectionFactory() diff --git a/NetSharp/NetSharp.Tests/RawStreamConnectionTests.cs b/NetSharp/NetSharp.Tests/RawStreamConnectionTests.cs @@ -13,72 +13,46 @@ namespace NetSharp.Tests { public class RawStreamConnectionTests { - private static readonly IPEndPoint ClientLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - private static readonly IPEndPoint ServerLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 12345); - [Fact] - public void DisposesCleanly() + public void ServerDisposesCleanly() { static void Instantiate() { - using RawStreamConnection conn = ConnectionFactory(); - conn.Bind(ClientLocalEndPoint); - - conn.Start(); + using RawStreamServer conn = ServerFactory(); + conn.Bind(TestUtils.ClientLocalEndPoint); conn.Close(); } Instantiate(); - AssertDisposedCleanly(); + TestUtils.AssertDisposedCleanly<RawStreamServer>(); } [Fact] - public void FullTest() + public void ClientDisposesCleanly() { - using RawStreamConnection server = ConnectionFactory(); - server.Bind(ServerLocalEndPoint); - - static void PacketHandler( - EndPoint point, - in RawPacketHeader header, - in ReadOnlyMemory<byte> data, - IRawStreamWriter writer) + static void Instantiate() { - } - - server.RegisterHandler(0, PacketHandler); - server.Start(); - - using RawStreamConnection client = ConnectionFactory(); - client.Bind(ClientLocalEndPoint); + using RawStreamClient conn = ClientFactory(); + conn.Bind(TestUtils.ClientLocalEndPoint); - client.ConnectAsync(ServerLocalEndPoint).GetAwaiter().GetResult(); - - byte[] packet = { 42 }; - _ = client.SendAsync(0, packet).GetAwaiter().GetResult(); - - client.DisconnectAsync().GetAwaiter().GetResult(); - client.Close(); + conn.Close(); + } - server.DeregisterHandler(0, PacketHandler); - server.Close(); + Instantiate(); - AssertDisposedCleanly(); + TestUtils.AssertDisposedCleanly<RawStreamClient>(); } - private static void AssertDisposedCleanly() + private static RawStreamServer ServerFactory() { - _ = dotMemory.Check(memory => - { - Assert.Equal(0, memory.GetObjects(where => where.Type.Is<RawStreamConnection>()).ObjectsCount); - }); + return new RawStreamServer(ProtocolType.Tcp, new IPEndPoint(IPAddress.Loopback, 0)); } - private static RawStreamConnection ConnectionFactory() + private static RawStreamClient ClientFactory() { - return new RawStreamConnection(ProtocolType.Tcp, new IPEndPoint(IPAddress.Loopback, 0)); + return new RawStreamClient(ProtocolType.Tcp, new IPEndPoint(IPAddress.Loopback, 0)); } } } diff --git a/NetSharp/NetSharp.Tests/TestUtils.cs b/NetSharp/NetSharp.Tests/TestUtils.cs @@ -0,0 +1,20 @@ +using JetBrains.dotMemoryUnit; +using System.Net; +using Xunit; + +namespace NetSharp.Tests +{ + internal static class TestUtils + { + internal static readonly IPEndPoint ClientLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + internal static readonly IPEndPoint ServerLocalEndPoint = new IPEndPoint(IPAddress.Loopback, 12345); + + internal static void AssertDisposedCleanly<T>() + { + _ = dotMemory.Check(memory => + { + Assert.Equal(0, memory.GetObjects(where => where.Type.Is<T>()).ObjectsCount); + }); + } + } +} diff --git a/NetSharp/NetSharp/NetSharp.xml b/NetSharp/NetSharp/NetSharp.xml @@ -52,9 +52,17 @@ <member name="M:NetSharp.Raw.Datagram.RawDatagramConnection.ResetSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Datagram.RawDatagramConnection.HandlerTaskWork"> + <member name="M:NetSharp.Raw.Datagram.RawDatagramConnection.Start(System.Int32)"> <inheritdoc /> </member> + <member name="M:NetSharp.Raw.IRawServer.Start(System.Int32)"> + <summary> + Starts listening to the network for incoming connections and data. + </summary> + <param name="concurrentTasks"> + The number of concurrent read tasks that should be maintained. + </param> + </member> <member name="T:NetSharp.Raw.RawConnectionBase"> <summary> Provides base functionality to all raw network connections. @@ -99,6 +107,11 @@ Whether the underlying network connection has been disposed or not. </summary> </member> + <member name="P:NetSharp.Raw.RawConnectionBase.IsBound"> + <summary> + Whether the underlying network connection has been bound to a local endpoint or not. + </summary> + </member> <member name="M:NetSharp.Raw.RawConnectionBase.Bind(System.Net.EndPoint)"> <summary> Binds the underlying network connection to the given local endpoint. @@ -115,14 +128,6 @@ <member name="M:NetSharp.Raw.RawConnectionBase.Dispose"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.RawConnectionBase.Start(System.Int32)"> - <summary> - Starts listening to the network for incoming connections and data. - </summary> - <param name="concurrentTasks"> - The number of concurrent read tasks that should be maintained. - </param> - </member> <member name="M:NetSharp.Raw.RawConnectionBase.CreateSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> <summary> Allows for custom initialisation of a pooled <see cref="T:System.Net.Sockets.SocketAsyncEventArgs" /> instance. @@ -147,11 +152,6 @@ Whether the <see cref="M:NetSharp.Raw.RawConnectionBase.Dispose" /> method was called. </param> </member> - <member name="M:NetSharp.Raw.RawConnectionBase.HandlerTaskWork"> - <summary> - Handler work delegate, started when a call to <see cref="M:NetSharp.Raw.RawConnectionBase.Start(System.Int32)" /> is made. - </summary> - </member> <member name="M:NetSharp.Raw.RawConnectionBase.RentBuffer(System.Int32)"> <summary> Rents a pooled buffer of at least the specified length. The buffer MUST be returned via a call to <see @@ -208,69 +208,65 @@ The number of concurrent read tasks that should be maintained. </param> </member> - <member name="T:NetSharp.Raw.RawPacketHeader"> - <summary> - Contains metadata about a network packet. - </summary> - </member> - <member name="F:NetSharp.Raw.RawPacketHeader.Length"> - <summary> - The total length of the packet when serialised. - </summary> - </member> - <member name="F:NetSharp.Raw.RawPacketHeader.DataLength"> + <member name="T:NetSharp.Raw.Stream.IRawStreamClient"> <summary> - The length of the data following this packet. - </summary> - </member> - <member name="F:NetSharp.Raw.RawPacketHeader.Type"> - <summary> - The packet type. + Describes the interface for a stream network connection that can write to the network. </summary> </member> - <member name="M:NetSharp.Raw.RawPacketHeader.op_Inequality(NetSharp.Raw.RawPacketHeader,NetSharp.Raw.RawPacketHeader)"> + <member name="M:NetSharp.Raw.Stream.IRawStreamClient.SendAsync(System.UInt32,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> <summary> - Checks whether two <see cref="T:NetSharp.Raw.RawPacketHeader" /> instances are not equal. + Writes the given packet header and data to the network asynchronously, using the given socket flags for the transmission. </summary> - <param name="left"> - The first instance. + <param name="type"> + The type of packet being written to the network. </param> - <param name="right"> - The second instance. + <param name="buffer"> + The data held by the packet being written to the network. + </param> + <param name="flags"> + The <see cref="T:System.Net.Sockets.SocketFlags" /> to use for the transmission. </param> <returns> - Whether the two instances are not equal. + The number of bytes written to the network. -1 if the object has been disposed. </returns> </member> - <member name="M:NetSharp.Raw.RawPacketHeader.op_Equality(NetSharp.Raw.RawPacketHeader,NetSharp.Raw.RawPacketHeader)"> + <member name="M:NetSharp.Raw.Stream.IRawStreamClient.ReceiveAsync(System.Memory{System.Byte},System.Net.Sockets.SocketFlags)"> <summary> - Checks whether two <see cref="T:NetSharp.Raw.RawPacketHeader" /> instances are equal. + Reads a packet from the network asynchronously, using the given socket flags for the transmission. </summary> - <param name="left"> - The first instance. + <param name="buffer"> + The data held by the packet being written to the network. </param> - <param name="right"> - The second instance. + <param name="flags"> + The <see cref="T:System.Net.Sockets.SocketFlags" /> to use for the transmission. </param> <returns> - Whether the two instances are equal. + The number of bytes written to the network. -1 if the object has been disposed. </returns> </member> - <member name="M:NetSharp.Raw.RawPacketHeader.Equals(System.Object)"> - <inheritdoc /> - </member> - <member name="M:NetSharp.Raw.RawPacketHeader.Equals(NetSharp.Raw.RawPacketHeader)"> - <inheritdoc /> - </member> - <member name="M:NetSharp.Raw.RawPacketHeader.GetHashCode"> - <inheritdoc /> + <member name="T:NetSharp.Raw.Stream.RawStreamPacketHandler"> + <summary> + Handles a message received on a raw stream connection. + </summary> + <param name="remoteEndPoint"> + The remote endpoint from which the received message originated. + </param> + <param name="header"> + The header of the received message. + </param> + <param name="data"> + The data held by the received message. + </param> + <param name="writer"> + A reference to the network connection, to interact with the network further. + </param> </member> <member name="T:NetSharp.Raw.Stream.IRawStreamPacketHandler"> <summary> Describes the interface for a stream network connection that can handle received packets. </summary> </member> - <member name="M:NetSharp.Raw.Stream.IRawStreamPacketHandler.DeregisterHandler(System.Int32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> + <member name="M:NetSharp.Raw.Stream.IRawStreamPacketHandler.DeregisterHandler(System.UInt32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> <summary> Deregisters a previously registered handler for the given packet type. No further invocations of the given handler will be made. @@ -282,7 +278,7 @@ The handler to deregister. </param> </member> - <member name="M:NetSharp.Raw.Stream.IRawStreamPacketHandler.RegisterHandler(System.Int32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> + <member name="M:NetSharp.Raw.Stream.IRawStreamPacketHandler.RegisterHandler(System.UInt32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> <summary> Registers the given handler for the given packet type. </summary> @@ -293,54 +289,71 @@ The handler to register. </param> </member> - <member name="T:NetSharp.Raw.Stream.IRawStreamWriter"> + <member name="T:NetSharp.Raw.Stream.RawPacketHeader"> <summary> - Describes the interface for a stream network connection that can write to the network. + Contains metadata about a network packet. </summary> </member> - <member name="M:NetSharp.Raw.Stream.IRawStreamWriter.SendAsync(System.UInt16,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> + <member name="F:NetSharp.Raw.Stream.RawPacketHeader.Length"> <summary> - Writes the given packet header and data to the network asynchronously, using the given socket flags for the transmission. + The total length of the packet when serialised. </summary> - <param name="type"> - The type of packet being written to the network. - </param> - <param name="buffer"> - The data held by the packet being written to the network. + </member> + <member name="F:NetSharp.Raw.Stream.RawPacketHeader.DataLength"> + <summary> + The length of the data following this packet. + </summary> + </member> + <member name="F:NetSharp.Raw.Stream.RawPacketHeader.Type"> + <summary> + The packet type. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawPacketHeader.op_Inequality(NetSharp.Raw.Stream.RawPacketHeader,NetSharp.Raw.Stream.RawPacketHeader)"> + <summary> + Checks whether two <see cref="T:NetSharp.Raw.Stream.RawPacketHeader" /> instances are not equal. + </summary> + <param name="left"> + The first instance. </param> - <param name="flags"> - The <see cref="T:System.Net.Sockets.SocketFlags" /> to use for the transmission. + <param name="right"> + The second instance. </param> <returns> - The number of bytes written to the network. + Whether the two instances are not equal. </returns> </member> - <member name="T:NetSharp.Raw.Stream.RawStreamPacketHandler"> + <member name="M:NetSharp.Raw.Stream.RawPacketHeader.op_Equality(NetSharp.Raw.Stream.RawPacketHeader,NetSharp.Raw.Stream.RawPacketHeader)"> <summary> - Handles a message received on a raw stream connection. + Checks whether two <see cref="T:NetSharp.Raw.Stream.RawPacketHeader" /> instances are equal. </summary> - <param name="remoteEndPoint"> - The remote endpoint from which the received message originated. - </param> - <param name="header"> - The header of the received message. - </param> - <param name="data"> - The data held by the received message. + <param name="left"> + The first instance. </param> - <param name="writer"> - A reference to the network connection, to interact with the network further. + <param name="right"> + The second instance. </param> + <returns> + Whether the two instances are equal. + </returns> </member> - <member name="T:NetSharp.Raw.Stream.RawStreamConnection"> + <member name="M:NetSharp.Raw.Stream.RawPacketHeader.Equals(System.Object)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawPacketHeader.Equals(NetSharp.Raw.Stream.RawPacketHeader)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawPacketHeader.GetHashCode"> + <inheritdoc /> + </member> + <member name="T:NetSharp.Raw.Stream.RawStreamClient"> <summary> - Represents a network connection using a stream-based protocol to interact over the network, that is capable of - sending raw bytes. + Represents a client using a stream-based protocol to interact with a server. Handles raw binary packets. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.#ctor(System.Net.Sockets.ProtocolType,System.Net.EndPoint)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.#ctor(System.Net.Sockets.ProtocolType,System.Net.EndPoint)"> <summary> - Initialises a new instance of the <see cref="T:NetSharp.Raw.Stream.RawStreamConnection" /> class. + Initialises a new instance of the <see cref="T:NetSharp.Raw.Stream.RawStreamClient" /> class. </summary> <param name="connectionProtocolType"> The protocol that the underlying network connection should use. @@ -349,7 +362,7 @@ The default remote endpoint that should be used for pending connections. </param> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ConnectAsync(System.Net.EndPoint)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ConnectAsync(System.Net.EndPoint)"> <summary> Connects asynchronously to the given remote network endpoint. </summary> @@ -360,10 +373,7 @@ A <see cref="T:System.Threading.Tasks.Task" /> object representing the asynchronous operation. </returns> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.DeregisterHandler(System.Int32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> - <inheritdoc /> - </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.DisconnectAsync(System.Boolean)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.DisconnectAsync(System.Boolean)"> <summary> Disconnects asynchronously from the currently connected remote network connection. </summary> @@ -374,128 +384,208 @@ A <see cref="T:System.Threading.Tasks.Task" /> object representing the asynchronous operation. </returns> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.RegisterHandler(System.Int32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> - <inheritdoc /> - </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.SendAsync(System.UInt16,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.SendAsync(System.UInt32,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.CreateSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ReceiveAsync(System.Memory{System.Byte},System.Net.Sockets.SocketFlags)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.DestroySocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.CreateSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.Dispose(System.Boolean)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.DestroySocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandlerTaskWork"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.Dispose(System.Boolean)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ResetSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ResetSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.StartHook(System.Int32)"> - <inheritdoc /> - </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ConfigureSendRequestAsync(System.Net.Sockets.SocketAsyncEventArgs,System.Byte[]@,NetSharp.Raw.RawPacketHeader@,System.ReadOnlyMemory{System.Byte}@,NetSharp.Raw.Stream.RawStreamConnection.WriterStateToken,System.Threading.Tasks.TaskCompletionSource{System.Int32})"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ConfigureSendRequestAsync(System.Net.Sockets.SocketAsyncEventArgs,System.Byte[]@,NetSharp.Raw.Stream.RawPacketHeader@,System.ReadOnlyMemory{System.Byte}@,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken,System.Threading.Tasks.TaskCompletionSource{System.Int32})"> <summary> Prepares the given socket args for sending a request to the network. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.CleanupArgs(System.Net.Sockets.SocketAsyncEventArgs,System.Boolean)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.CleanupArgs(System.Net.Sockets.SocketAsyncEventArgs,System.Boolean)"> <summary> Cleans up and returns the given socket args. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ConfigureReceiveDataAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken,NetSharp.Raw.RawPacketHeader@)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ConfigureReceiveDataAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken,NetSharp.Raw.Stream.RawPacketHeader@)"> <summary> Prepares the given socket args for receiving a packet's data from the network. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ConfigureReceiveHeaderAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.ConfigureReceiveHeaderAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken)"> <summary> Prepares the given socket args for receiving a packet's header from the network. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleAccepted(System.Net.Sockets.SocketAsyncEventArgs)"> - <summary> - Handles a completed <see cref="M:System.Net.Sockets.Socket.AcceptAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. - </summary> - </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleConnected(System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleConnected(System.Net.Sockets.SocketAsyncEventArgs)"> <summary> Handles a completed <see cref="M:System.Net.Sockets.Socket.ConnectAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleDisconnected(System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleDisconnected(System.Net.Sockets.SocketAsyncEventArgs)"> <summary> Handles a completed <see cref="M:System.Net.Sockets.Socket.DisconnectAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleIoCompleted(System.Object,System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleIoCompleted(System.Object,System.Net.Sockets.SocketAsyncEventArgs)"> <summary> Handles the completion of an asynchronous socket operation. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleReceived(System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleReceived(System.Net.Sockets.SocketAsyncEventArgs)"> <summary> Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleReceivedData(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleReceivedData(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken)"> <summary> Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when receiving a packet's data from the network. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleReceivedHeader(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleReceivedHeader(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken)"> <summary> Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when receiving a packet's header from the network. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleSent(System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleSent(System.Net.Sockets.SocketAsyncEventArgs)"> <summary> Handles the completion of a <see cref="M:System.Net.Sockets.Socket.SendAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleSentRequest(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.WriterStateToken)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.HandleSentRequest(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamClient.WriterStateToken)"> <summary> Handles the completion of a <see cref="M:System.Net.Sockets.Socket.SendAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when sending a request packet to the network. In this case, the <see cref="P:System.Net.Sockets.SocketAsyncEventArgs.ConnectSocket" /> will be used to perform the transmission. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.HandleSentResponse(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.StartOrContinueReceiving(System.Net.Sockets.SocketAsyncEventArgs)"> <summary> - Handles the completion of a <see cref="M:System.Net.Sockets.Socket.SendAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when sending a response packet to the - network. In this case, the <see cref="P:System.Net.Sockets.SocketAsyncEventArgs.AcceptSocket" /> will be used to perform the transmission. + Starts or continues an asynchronous network read operation. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.StartOrContinueReceiving(System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.StartOrContinueSending(System.Net.Sockets.Socket,System.Net.Sockets.SocketAsyncEventArgs)"> <summary> - Starts or continues an asynchronous network read operation. + Starts or continues an asynchronous network write operation using the given socket. </summary> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.StartOrContinueSending(System.Net.Sockets.Socket,System.Net.Sockets.SocketAsyncEventArgs)"> + <member name="M:NetSharp.Raw.Stream.RawStreamClient.WriterStateToken.Dispose"> + <inheritdoc /> + </member> + <member name="T:NetSharp.Raw.Stream.RawStreamServer"> <summary> - Starts or continues an asynchronous network write operation using the given socket. + Represents a server using a stream-based protocol to interact with clients. Handles raw binary packets. </summary> </member> - <member name="P:NetSharp.Raw.Stream.RawStreamConnection.OperationStateToken.OperationCompletionSource"> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.#ctor(System.Net.Sockets.ProtocolType,System.Net.EndPoint)"> <summary> - The <see cref="T:System.Threading.Tasks.TaskCompletionSource`1" /> for asynchronous network operations. + Initialises a new instance of the <see cref="T:NetSharp.Raw.Stream.RawStreamServer" /> class. </summary> + <param name="connectionProtocolType"> + The protocol that the underlying network connection should use. + </param> + <param name="defaultRemoteEndPoint"> + The default remote endpoint that should be used for pending connections. + </param> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.DeregisterHandler(System.UInt32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.RegisterHandler(System.UInt32,NetSharp.Raw.Stream.RawStreamPacketHandler)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.Start(System.Int32)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.CreateSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.DestroySocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.OperationStateToken.Dispose"> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.Dispose(System.Boolean)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.ReaderStateToken.Dispose"> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.ResetSocketArgsHook(System.Net.Sockets.SocketAsyncEventArgs@)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.StartHook(System.Int32)"> + <inheritdoc /> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.CleanupArgs(System.Net.Sockets.SocketAsyncEventArgs,System.Boolean)"> + <summary> + Cleans up and returns the given socket args. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.ConfigureReceiveDataAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken,NetSharp.Raw.Stream.RawPacketHeader@)"> + <summary> + Prepares the given socket args for receiving a packet's data from the network. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.ConfigureReceiveHeaderAsync(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken)"> + <summary> + Prepares the given socket args for receiving a packet's header from the network. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleAccepted(System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Handles a completed <see cref="M:System.Net.Sockets.Socket.AcceptAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleIoCompleted(System.Object,System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Handles the completion of an asynchronous socket operation. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleReceived(System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleReceivedData(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken)"> + <summary> + Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when receiving a packet's data from the network. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleReceivedHeader(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken)"> + <summary> + Handles the completion of a <see cref="M:System.Net.Sockets.Socket.ReceiveAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when receiving a packet's header from + the network. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleSent(System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Handles the completion of a <see cref="M:System.Net.Sockets.Socket.SendAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.HandleSentResponse(System.Net.Sockets.SocketAsyncEventArgs,NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken)"> + <summary> + Handles the completion of a <see cref="M:System.Net.Sockets.Socket.SendAsync(System.Net.Sockets.SocketAsyncEventArgs)" /> call, when sending a response packet to the + network. In this case, the <see cref="P:System.Net.Sockets.SocketAsyncEventArgs.AcceptSocket" /> will be used to perform the transmission. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.StartOrContinueReceiving(System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Starts or continues an asynchronous network read operation. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.StartOrContinueSending(System.Net.Sockets.Socket,System.Net.Sockets.SocketAsyncEventArgs)"> + <summary> + Starts or continues an asynchronous network write operation using the given socket. + </summary> + </member> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.ReaderStateToken.Dispose"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.RemoteConnectionWrapper.SendAsync(System.UInt16,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.RemoteConnectionWrapper.ReceiveAsync(System.Memory{System.Byte},System.Net.Sockets.SocketFlags)"> <inheritdoc /> </member> - <member name="M:NetSharp.Raw.Stream.RawStreamConnection.WriterStateToken.Dispose"> + <member name="M:NetSharp.Raw.Stream.RawStreamServer.RemoteConnectionWrapper.SendAsync(System.UInt32,System.ReadOnlyMemory{System.Byte},System.Net.Sockets.SocketFlags)"> <inheritdoc /> </member> <member name="T:NetSharp.Utils.SlimObjectPool`1"> diff --git a/NetSharp/NetSharp/Raw/Datagram/RawDatagramConnection.cs b/NetSharp/NetSharp/Raw/Datagram/RawDatagramConnection.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; +using NetSharp.Raw.Stream; namespace NetSharp.Raw.Datagram { @@ -78,9 +80,18 @@ namespace NetSharp.Raw.Datagram } /// <inheritdoc /> - protected override void HandlerTaskWork() + public void Start(int concurrentTasks = 1) { - // TODO: implement start read task for datagram connections + Debug.Assert(IsBound, "Connection must be bound before we can start listening to the network!"); + + StartHook(concurrentTasks); + + int taskCount = concurrentTasks < 1 ? 1 : concurrentTasks; + + for (int i = 0; i < taskCount; i++) + { + // TODO: implement task + } } private void HandleIoCompleted(object sender, SocketAsyncEventArgs args) diff --git a/NetSharp/NetSharp/Raw/IRawServer.cs b/NetSharp/NetSharp/Raw/IRawServer.cs @@ -0,0 +1,13 @@ +namespace NetSharp.Raw +{ + public interface IRawServer + { + /// <summary> + /// Starts listening to the network for incoming connections and data. + /// </summary> + /// <param name="concurrentTasks"> + /// The number of concurrent read tasks that should be maintained. + /// </param> + public void Start(int concurrentTasks = 1); + } +} +\ No newline at end of file diff --git a/NetSharp/NetSharp/Raw/RawConnectionBase.cs b/NetSharp/NetSharp/Raw/RawConnectionBase.cs @@ -17,9 +17,8 @@ namespace NetSharp.Raw private readonly ArrayPool<byte> bufferPool; private readonly Socket connection; private readonly SlimObjectPool<SocketAsyncEventArgs> socketArgsPool; - - private int activeOperations; - private readonly object activeOperationsLock = new object(); + private readonly object rentedSocketArgsLock = new object(); + private int rentedSocketArgs; private bool isBound; private bool isDisposed; @@ -76,6 +75,11 @@ namespace NetSharp.Raw protected bool IsDisposed => isDisposed; /// <summary> + /// Whether the underlying network connection has been bound to a local endpoint or not. + /// </summary> + protected bool IsBound => isBound; + + /// <summary> /// Binds the underlying network connection to the given local endpoint. /// </summary> /// <param name="localEndPoint"> @@ -104,26 +108,6 @@ namespace NetSharp.Raw } /// <summary> - /// Starts listening to the network for incoming connections and data. - /// </summary> - /// <param name="concurrentTasks"> - /// The number of concurrent read tasks that should be maintained. - /// </param> - public void Start(int concurrentTasks = 1) - { - Debug.Assert(isBound, "Connection must be bound before we can start listening to the network!"); - - StartHook(concurrentTasks); - - int taskCount = concurrentTasks < 1 ? 1 : concurrentTasks; - - for (int i = 0; i < taskCount; i++) - { - HandlerTaskWork(); - } - } - - /// <summary> /// Allows for custom initialisation of a pooled <see cref="SocketAsyncEventArgs" /> instance. /// </summary> /// <param name="instance"> @@ -160,28 +144,22 @@ namespace NetSharp.Raw { connection.Dispose(); - lock (activeOperationsLock) + lock (rentedSocketArgsLock) { - while (activeOperations > 0) + while (rentedSocketArgs > 0) { - // will keep reacquiring the lock and blocking until we reach the activeOperations == 0 case - _ = Monitor.Wait(activeOperationsLock); + // will keep reacquiring the lock and blocking until we reach the rentedSocketArgs == 0 case + _ = Monitor.Wait(rentedSocketArgsLock); } } socketArgsPool.Dispose(); } - // TODO: Set large fields to null isDisposed = true; } /// <summary> - /// Handler work delegate, started when a call to <see cref="Start" /> is made. - /// </summary> - protected abstract void HandlerTaskWork(); - - /// <summary> /// Rents a pooled buffer of at least the specified length. The buffer MUST be returned via a call to <see /// cref="ReturnBuffer(byte[], bool)" /> once it has been used. /// </summary> @@ -205,7 +183,7 @@ namespace NetSharp.Raw /// </returns> protected SocketAsyncEventArgs RentSocketArgs() { - _ = Interlocked.Increment(ref activeOperations); + _ = Interlocked.Increment(ref rentedSocketArgs); return socketArgsPool.Rent(); } @@ -244,10 +222,10 @@ namespace NetSharp.Raw { socketArgsPool.Return(socketArgs); - lock (activeOperationsLock) + lock (rentedSocketArgsLock) { - _ = Interlocked.Decrement(ref activeOperations); - Monitor.Pulse(activeOperationsLock); // signals that we might have reached the activeOperations == 0 state + rentedSocketArgs--; + Monitor.Pulse(rentedSocketArgsLock); // signals that we might have reached the rentedSocketArgs == 0 state } } @@ -279,9 +257,9 @@ namespace NetSharp.Raw private void ResetSocketArgs(ref SocketAsyncEventArgs instance) { - ResetSocketArgsHook(ref instance); - instance.RemoteEndPoint = DefaultRemoteEndPoint; + + ResetSocketArgsHook(ref instance); } } } diff --git a/NetSharp/NetSharp/Raw/RawPacket.cs b/NetSharp/NetSharp/Raw/RawPacket.cs @@ -1,30 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace NetSharp.Raw -{ - internal static class RawPacket - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Serialise( - in Memory<byte> backingBuffer, - in RawPacketHeader packetHeader, - in ReadOnlyMemory<byte> packetData) - { - Debug.Assert( - backingBuffer.Length >= RawPacketHeader.Length + packetData.Length, - "Attempted to serialise packet to an undersized buffer!"); - - packetHeader.Serialise(backingBuffer.Span.Slice(0, RawPacketHeader.Length)); - - packetData.CopyTo(backingBuffer.Slice(RawPacketHeader.Length, packetData.Length)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int TotalSize(in RawPacketHeader packetHeader) - { - return RawPacketHeader.Length + packetHeader.DataLength; - } - } -} diff --git a/NetSharp/NetSharp/Raw/RawPacketHeader.cs b/NetSharp/NetSharp/Raw/RawPacketHeader.cs @@ -1,115 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace NetSharp.Raw -{ - /// <summary> - /// Contains metadata about a network packet. - /// </summary> - public readonly struct RawPacketHeader : IEquatable<RawPacketHeader> - { - /// <summary> - /// The total length of the packet when serialised. - /// </summary> - public const int Length = sizeof(ushort) + sizeof(int); - -#pragma warning disable CA1051 - - /// <summary> - /// The length of the data following this packet. - /// </summary> - public readonly int DataLength; - - /// <summary> - /// The packet type. - /// </summary> - public readonly ushort Type; - -#pragma warning restore CA1051 - - internal RawPacketHeader(ushort type, int dataLength) - { - Type = type; - DataLength = dataLength; - } - - /// <summary> - /// Checks whether two <see cref="RawPacketHeader" /> instances are not equal. - /// </summary> - /// <param name="left"> - /// The first instance. - /// </param> - /// <param name="right"> - /// The second instance. - /// </param> - /// <returns> - /// Whether the two instances are not equal. - /// </returns> - public static bool operator !=(RawPacketHeader left, RawPacketHeader right) - { - return !(left == right); - } - - /// <summary> - /// Checks whether two <see cref="RawPacketHeader" /> instances are equal. - /// </summary> - /// <param name="left"> - /// The first instance. - /// </param> - /// <param name="right"> - /// The second instance. - /// </param> - /// <returns> - /// Whether the two instances are equal. - /// </returns> - public static bool operator ==(RawPacketHeader left, RawPacketHeader right) - { - return left.Equals(right); - } - - /// <inheritdoc /> - public override bool Equals(object obj) - { - return obj is RawPacketHeader other && Equals(other); - } - - /// <inheritdoc /> - public bool Equals(RawPacketHeader other) - { - return DataLength == other.DataLength && Type == other.Type; - } - - /// <inheritdoc /> - public override int GetHashCode() - { - return HashCode.Combine(DataLength, Type); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static RawPacketHeader Deserialise(in ReadOnlySpan<byte> buffer) - { - int offset = 0; - - ushort type = MemoryMarshal.Read<ushort>(buffer.Slice(offset, sizeof(ushort))); - offset += sizeof(ushort); - - int dataSize = MemoryMarshal.Read<int>(buffer.Slice(offset, sizeof(int))); - - return new RawPacketHeader(type, dataSize); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Serialise(in Span<byte> buffer) - { - int offset = 0; - - ushort type = Type; - MemoryMarshal.Write(buffer.Slice(offset, sizeof(ushort)), ref type); - offset += sizeof(ushort); - - int dataSize = DataLength; - MemoryMarshal.Write(buffer.Slice(offset, sizeof(int)), ref dataSize); - } - } -} diff --git a/NetSharp/NetSharp/Raw/Stream/IRawStreamClient.cs b/NetSharp/NetSharp/Raw/Stream/IRawStreamClient.cs @@ -0,0 +1,67 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace NetSharp.Raw.Stream +{ + // TODO(mikolaj): see about adding an enum for possible connection closure reasons + public readonly struct RawStreamWriteAsyncResult + { + public readonly int WrittenBytes; + + public RawStreamWriteAsyncResult(int writtenBytes) + { + WrittenBytes = writtenBytes; + } + } + + // TODO(mikolaj): see about adding an enum for possible connection closure reasons + public readonly struct RawStreamReadAsyncResult + { + public readonly int ReadBytes; + public readonly uint PacketType; + + public RawStreamReadAsyncResult(int readBytes, uint packetType) + { + ReadBytes = readBytes; + PacketType = packetType; + } + } + + /// <summary> + /// Describes the interface for a stream network connection that can write to the network. + /// </summary> + public interface IRawStreamClient + { + /// <summary> + /// Writes the given packet header and data to the network asynchronously, using the given socket flags for the transmission. + /// </summary> + /// <param name="type"> + /// The type of packet being written to the network. + /// </param> + /// <param name="buffer"> + /// The data held by the packet being written to the network. + /// </param> + /// <param name="flags"> + /// The <see cref="SocketFlags" /> to use for the transmission. + /// </param> + /// <returns> + /// The number of bytes written to the network. -1 if the object has been disposed. + /// </returns> + ValueTask<RawStreamWriteAsyncResult> SendAsync(uint type, ReadOnlyMemory<byte> buffer, SocketFlags flags = SocketFlags.None); + + /// <summary> + /// Reads a packet from the network asynchronously, using the given socket flags for the transmission. + /// </summary> + /// <param name="buffer"> + /// The data held by the packet being written to the network. + /// </param> + /// <param name="flags"> + /// The <see cref="SocketFlags" /> to use for the transmission. + /// </param> + /// <returns> + /// The number of bytes written to the network. -1 if the object has been disposed. + /// </returns> + ValueTask<RawStreamReadAsyncResult> ReceiveAsync(Memory<byte> buffer, SocketFlags flags = SocketFlags.None); + } +} diff --git a/NetSharp/NetSharp/Raw/Stream/IRawStreamPacketHandler.cs b/NetSharp/NetSharp/Raw/Stream/IRawStreamPacketHandler.cs @@ -1,6 +1,30 @@ -namespace NetSharp.Raw.Stream +using System; +using System.Net; + +namespace NetSharp.Raw.Stream { /// <summary> + /// Handles a message received on a raw stream connection. + /// </summary> + /// <param name="remoteEndPoint"> + /// The remote endpoint from which the received message originated. + /// </param> + /// <param name="header"> + /// The header of the received message. + /// </param> + /// <param name="data"> + /// The data held by the received message. + /// </param> + /// <param name="writer"> + /// A reference to the network connection, to interact with the network further. + /// </param> + public delegate void RawStreamPacketHandler( + EndPoint remoteEndPoint, + in RawPacketHeader header, + in ReadOnlyMemory<byte> data, + IRawStreamClient writer); + + /// <summary> /// Describes the interface for a stream network connection that can handle received packets. /// </summary> public interface IRawStreamPacketHandler @@ -15,7 +39,7 @@ /// <param name="handler"> /// The handler to deregister. /// </param> - void DeregisterHandler(int id, RawStreamPacketHandler handler); + void DeregisterHandler(uint id, RawStreamPacketHandler handler); /// <summary> /// Registers the given handler for the given packet type. @@ -26,6 +50,6 @@ /// <param name="handler"> /// The handler to register. /// </param> - void RegisterHandler(int id, RawStreamPacketHandler handler); + void RegisterHandler(uint id, RawStreamPacketHandler handler); } } diff --git a/NetSharp/NetSharp/Raw/Stream/IRawStreamWriter.cs b/NetSharp/NetSharp/Raw/Stream/IRawStreamWriter.cs @@ -1,29 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace NetSharp.Raw.Stream -{ - /// <summary> - /// Describes the interface for a stream network connection that can write to the network. - /// </summary> - public interface IRawStreamWriter - { - /// <summary> - /// Writes the given packet header and data to the network asynchronously, using the given socket flags for the transmission. - /// </summary> - /// <param name="type"> - /// The type of packet being written to the network. - /// </param> - /// <param name="buffer"> - /// The data held by the packet being written to the network. - /// </param> - /// <param name="flags"> - /// The <see cref="SocketFlags" /> to use for the transmission. - /// </param> - /// <returns> - /// The number of bytes written to the network. - /// </returns> - ValueTask<int> SendAsync(ushort type, ReadOnlyMemory<byte> buffer, SocketFlags flags = SocketFlags.None); - } -} diff --git a/NetSharp/NetSharp/Raw/Stream/RawPacket.cs b/NetSharp/NetSharp/Raw/Stream/RawPacket.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace NetSharp.Raw.Stream +{ + internal static class RawPacket + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Serialise( + in Memory<byte> backingBuffer, + in RawPacketHeader packetHeader, + in ReadOnlyMemory<byte> packetData) + { + Debug.Assert( + backingBuffer.Length >= RawPacketHeader.Length + packetData.Length, + "Attempted to serialise packet to an undersized buffer!"); + + packetHeader.Serialise(backingBuffer.Span.Slice(0, RawPacketHeader.Length)); + + packetData.CopyTo(backingBuffer.Slice(RawPacketHeader.Length, packetData.Length)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int TotalSize(in RawPacketHeader packetHeader) + { + return RawPacketHeader.Length + packetHeader.DataLength; + } + } +} diff --git a/NetSharp/NetSharp/Raw/Stream/RawPacketHeader.cs b/NetSharp/NetSharp/Raw/Stream/RawPacketHeader.cs @@ -0,0 +1,115 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace NetSharp.Raw.Stream +{ + /// <summary> + /// Contains metadata about a network packet. + /// </summary> + public readonly struct RawPacketHeader : IEquatable<RawPacketHeader> + { + /// <summary> + /// The total length of the packet when serialised. + /// </summary> + public const int Length = sizeof(int) + sizeof(uint); + +#pragma warning disable CA1051 + + /// <summary> + /// The length of the data following this packet. + /// </summary> + public readonly int DataLength; + + /// <summary> + /// The packet type. + /// </summary> + public readonly uint Type; + +#pragma warning restore CA1051 + + internal RawPacketHeader(uint type, int dataLength) + { + Type = type; + DataLength = dataLength; + } + + /// <summary> + /// Checks whether two <see cref="RawPacketHeader" /> instances are not equal. + /// </summary> + /// <param name="left"> + /// The first instance. + /// </param> + /// <param name="right"> + /// The second instance. + /// </param> + /// <returns> + /// Whether the two instances are not equal. + /// </returns> + public static bool operator !=(RawPacketHeader left, RawPacketHeader right) + { + return !(left == right); + } + + /// <summary> + /// Checks whether two <see cref="RawPacketHeader" /> instances are equal. + /// </summary> + /// <param name="left"> + /// The first instance. + /// </param> + /// <param name="right"> + /// The second instance. + /// </param> + /// <returns> + /// Whether the two instances are equal. + /// </returns> + public static bool operator ==(RawPacketHeader left, RawPacketHeader right) + { + return left.Equals(right); + } + + /// <inheritdoc /> + public override bool Equals(object obj) + { + return obj is RawPacketHeader other && Equals(other); + } + + /// <inheritdoc /> + public bool Equals(RawPacketHeader other) + { + return DataLength == other.DataLength && Type == other.Type; + } + + /// <inheritdoc /> + public override int GetHashCode() + { + return HashCode.Combine(DataLength, Type); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static RawPacketHeader Deserialise(in ReadOnlySpan<byte> buffer) + { + int offset = 0; + + uint type = MemoryMarshal.Read<uint>(buffer.Slice(offset, sizeof(uint))); + offset += sizeof(ushort); + + int dataSize = MemoryMarshal.Read<int>(buffer.Slice(offset, sizeof(int))); + + return new RawPacketHeader(type, dataSize); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Serialise(in Span<byte> buffer) + { + int offset = 0; + + uint type = Type; + MemoryMarshal.Write(buffer.Slice(offset, sizeof(uint)), ref type); + offset += sizeof(uint); + + int dataSize = DataLength; + MemoryMarshal.Write(buffer.Slice(offset, sizeof(int)), ref dataSize); + } + } +} diff --git a/NetSharp/NetSharp/Raw/Stream/RawStreamClient.cs b/NetSharp/NetSharp/Raw/Stream/RawStreamClient.cs @@ -0,0 +1,551 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using NetSharp.Utils; + +namespace NetSharp.Raw.Stream +{ + /// <summary> + /// Represents a client using a stream-based protocol to interact with a server. Handles raw binary packets. + /// </summary> + public sealed class RawStreamClient : RawConnectionBase, IRawStreamClient + { + private readonly SlimObjectPool<WriterStateToken> writerStatePool; + + /// <summary> + /// Initialises a new instance of the <see cref="RawStreamClient" /> class. + /// </summary> + /// <param name="connectionProtocolType"> + /// The protocol that the underlying network connection should use. + /// </param> + /// <param name="defaultRemoteEndPoint"> + /// The default remote endpoint that should be used for pending connections. + /// </param> + public RawStreamClient(ProtocolType connectionProtocolType, EndPoint defaultRemoteEndPoint) + : base(SocketType.Stream, connectionProtocolType, defaultRemoteEndPoint) + { + static WriterStateToken CreateWriterToken() + { + return new WriterStateToken(); + } + + static void ResetWriterToken(ref WriterStateToken instance) + { + instance.Reset(); + } + + static void DestroyWriterToken(WriterStateToken instance) + { + instance.Dispose(); + } + + writerStatePool = new SlimObjectPool<WriterStateToken>( + CreateWriterToken, + ResetWriterToken, + DestroyWriterToken); + } + + /// <summary> + /// Connects asynchronously to the given remote network endpoint. + /// </summary> + /// <param name="remoteEndPoint"> + /// The endpoint of the remote network connection to which we want to connect. + /// </param> + /// <returns> + /// A <see cref="Task" /> object representing the asynchronous operation. + /// </returns> + public Task ConnectAsync(EndPoint remoteEndPoint) + { + TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + SocketAsyncEventArgs socketArgs = RentSocketArgs(); + + socketArgs.UserToken = tcs; + + socketArgs.RemoteEndPoint = remoteEndPoint; + + if (Connection.ConnectAsync(socketArgs)) + { + return tcs.Task; + } + + CleanupArgs(socketArgs); + + return Task.CompletedTask; + } + + /// <summary> + /// Disconnects asynchronously from the currently connected remote network connection. + /// </summary> + /// <param name="leaveConnectionReusable"> + /// Whether the underlying network connection should be left in a reusable state after this call completes. + /// </param> + /// <returns> + /// A <see cref="Task" /> object representing the asynchronous operation. + /// </returns> + public Task DisconnectAsync(bool leaveConnectionReusable = false) + { + TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + SocketAsyncEventArgs socketArgs = RentSocketArgs(); + + socketArgs.UserToken = tcs; + + socketArgs.DisconnectReuseSocket = leaveConnectionReusable; + + if (Connection.DisconnectAsync(socketArgs)) + { + return tcs.Task; + } + + CleanupArgs(socketArgs); + + return Task.CompletedTask; + } + + /// <inheritdoc /> + public ValueTask<RawStreamWriteAsyncResult> SendAsync(uint type, ReadOnlyMemory<byte> buffer, SocketFlags flags) + { + TaskCompletionSource<RawStreamWriteAsyncResult> tcs = new TaskCompletionSource<RawStreamWriteAsyncResult>(); + SocketAsyncEventArgs socketArgs = RentSocketArgs(); + + RawPacketHeader header = new RawPacketHeader(type, buffer.Length); + byte[] ownedBuffer = RentBuffer(RawPacket.TotalSize(in header)); + + WriterStateToken writerState = writerStatePool.Rent(); + + //ConfigureSendRequestAsync(socketArgs, ref ownedBuffer, in header, in buffer, writerState, tcs); + + socketArgs.SocketFlags = flags; + + StartOrContinueSending(Connection, socketArgs); + + return new ValueTask<RawStreamWriteAsyncResult>(tcs.Task); + } + + /// <inheritdoc /> + public ValueTask<RawStreamReadAsyncResult> ReceiveAsync(Memory<byte> buffer, SocketFlags flags = SocketFlags.None) + { + TaskCompletionSource<RawStreamReadAsyncResult> tcs = new TaskCompletionSource<RawStreamReadAsyncResult>(); + SocketAsyncEventArgs socketArgs = RentSocketArgs(); + + byte[] ownedBuffer = RentBuffer(RawPacketHeader.Length); + + WriterStateToken writerState = writerStatePool.Rent(); + + throw new NotImplementedException(); + } + + /// <inheritdoc /> + protected override void CreateSocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.Completed += HandleIoCompleted; + + base.CreateSocketArgsHook(ref instance); + } + + /// <inheritdoc /> + protected override void DestroySocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.Completed -= HandleIoCompleted; + + base.DestroySocketArgsHook(ref instance); + } + + /// <inheritdoc /> + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + if (disposing) + { + // TODO(mikolaj): close remote connection + writerStatePool.Dispose(); + } + + base.Dispose(disposing); + } + + /// <inheritdoc /> + protected override void ResetSocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.AcceptSocket = null; + + base.ResetSocketArgsHook(ref instance); + } + + /// <summary> + /// Prepares the given socket args for sending a request to the network. + /// </summary> + private static void ConfigureSendRequestAsync( + SocketAsyncEventArgs args, + ref byte[] ownedBuffer, + in RawPacketHeader pendingHeader, + in ReadOnlyMemory<byte> pendingData, + WriterStateToken writerState, + TaskCompletionSource<int> tcs) + { + Memory<byte> ownedBufferMemory = new Memory<byte>(ownedBuffer); + RawPacket.Serialise(in ownedBufferMemory, in pendingHeader, in pendingData); + + int totalTransferredBytes = RawPacket.TotalSize(in pendingHeader); + args.SetBuffer(ownedBuffer, 0, totalTransferredBytes); + + writerState.BytesToTransfer = totalTransferredBytes; + writerState.RequestCompletionSource = tcs; + + args.UserToken = writerState; + } + + /// <summary> + /// Cleans up and returns the given socket args. + /// </summary> + private void CleanupArgs(SocketAsyncEventArgs args, bool cleanupUserToken = true) + { + if (cleanupUserToken) + { + switch (args.UserToken) + { + case WriterStateToken writerState: + writerStatePool.Return(writerState); + break; + } + } + + ReturnSocketArgs(args); + } + + /// <summary> + /// Prepares the given socket args for receiving a packet's data from the network. + /// </summary> + private void ConfigureReceiveDataAsync(SocketAsyncEventArgs args, WriterStateToken writerState, in RawPacketHeader header) + { + ReturnBuffer(args.Buffer); // return and clear the previously parsed request header buffer + + byte[] pendingDataBuffer = RentBuffer(header.DataLength); + + args.SetBuffer(pendingDataBuffer, 0, header.DataLength); + + writerState.BytesToTransfer = header.DataLength; + writerState.Header = header; + + args.UserToken = writerState; + } + + /// <summary> + /// Prepares the given socket args for receiving a packet's header from the network. + /// </summary> + private void ConfigureReceiveHeaderAsync(SocketAsyncEventArgs args, WriterStateToken writerState) + { + ReturnBuffer(args.Buffer); // return and clear the previously sent response packet buffer + + byte[] pendingHeaderBuffer = RentBuffer(RawPacketHeader.Length); + + args.SetBuffer(pendingHeaderBuffer, 0, RawPacketHeader.Length); + + writerState.BytesToTransfer = RawPacketHeader.Length; + + args.UserToken = writerState; + } + + /// <summary> + /// Handles a completed <see cref="Socket.ConnectAsync(SocketAsyncEventArgs)" /> call. + /// </summary> + private void HandleConnected(SocketAsyncEventArgs args) + { + TaskCompletionSource<bool> tcs = (TaskCompletionSource<bool>)args.UserToken; + + switch (args.SocketError) + { + case SocketError.Success: + tcs.SetResult(true); + break; + + case SocketError.OperationAborted: + tcs.SetCanceled(); + break; + + default: + tcs.SetException(new SocketException((int)args.SocketError)); + break; + } + + CleanupArgs(args); + } + + /// <summary> + /// Handles a completed <see cref="Socket.DisconnectAsync" /> call. + /// </summary> + private void HandleDisconnected(SocketAsyncEventArgs args) + { + TaskCompletionSource<bool> tcs = (TaskCompletionSource<bool>)args.UserToken; + + switch (args.SocketError) + { + case SocketError.Success: + tcs.SetResult(true); + break; + + case SocketError.OperationAborted: + tcs.SetCanceled(); + break; + + default: + tcs.SetException(new SocketException((int)args.SocketError)); + break; + } + + CleanupArgs(args); + } + + /// <summary> + /// Handles the completion of an asynchronous socket operation. + /// </summary> + private void HandleIoCompleted(object sender, SocketAsyncEventArgs args) + { + switch (args.LastOperation) + { + case SocketAsyncOperation.Connect: + HandleConnected(args); + break; + + case SocketAsyncOperation.Disconnect: + HandleDisconnected(args); + break; + + case SocketAsyncOperation.Receive: + HandleReceived(args); + break; + + case SocketAsyncOperation.Send: + HandleSent(args); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call. + /// </summary> + private void HandleReceived(SocketAsyncEventArgs args) + { + WriterStateToken writerState = (WriterStateToken)args.UserToken; + TaskCompletionSource<int> tcs = writerState.RequestCompletionSource!; + + switch (args.SocketError) + { + case SocketError.Success: + switch (writerState.BytesToTransfer) + { + case RawPacketHeader.Length: + HandleReceivedHeader(args, writerState); + break; + + default: + HandleReceivedData(args, writerState); + break; + } + + break; + + default: + tcs.SetException(new SocketException((int)args.SocketError)); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's data from the network. + /// </summary> + private void HandleReceivedData(SocketAsyncEventArgs args, WriterStateToken writerState) + { + TaskCompletionSource<int> tcs = writerState.RequestCompletionSource!; + + int received = args.BytesTransferred; + int previouslyReceived = args.Offset; + int totalReceived = previouslyReceived + received; + int expected = writerState.BytesToTransfer; + + if (totalReceived == expected) + { + // TODO(mikolaj): copy over received bytes to user memory buffer + tcs.SetResult(totalReceived - RawPacketHeader.Length); + CleanupArgs(args); + } + else if (totalReceived > 0 && totalReceived < expected) + { + args.SetBuffer(totalReceived, expected - totalReceived); + StartOrContinueSending(args.ConnectSocket, args); + } + else if (received == 0) + { + // connection is dead + tcs.SetException(new SocketException((int)SocketError.HostDown)); + CleanupArgs(args); + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's header from + /// the network. + /// </summary> + private void HandleReceivedHeader(SocketAsyncEventArgs args, WriterStateToken writerState) + { + TaskCompletionSource<int> tcs = writerState.RequestCompletionSource!; + + int received = args.BytesTransferred; + int previouslyReceived = args.Offset; + int totalReceived = previouslyReceived + received; + int expected = writerState.BytesToTransfer; + + byte[] headerBuffer = args.Buffer; + ReadOnlySpan<byte> headerBufferMemory = new ReadOnlySpan<byte>(headerBuffer); + + if (totalReceived == expected) + { + RawPacketHeader header = RawPacketHeader.Deserialise(in headerBufferMemory); + + ConfigureReceiveDataAsync(args, writerState, in header); + StartOrContinueReceiving(args); + } + else if (totalReceived > 0 && totalReceived < expected) + { + args.SetBuffer(totalReceived, expected - totalReceived); + StartOrContinueReceiving(args); + } + else if (received == 0) + { + tcs.SetException(new SocketException((int)args.SocketError)); + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.SendAsync" /> call. + /// </summary> + private void HandleSent(SocketAsyncEventArgs args) + { + WriterStateToken writerState = (WriterStateToken)args.UserToken; + + switch (args.SocketError) + { + case SocketError.Success: + HandleSentRequest(args, writerState); + break; + + case SocketError.OperationAborted: + writerState.RequestCompletionSource!.SetCanceled(); + CleanupArgs(args); + break; + + default: + writerState.RequestCompletionSource!.SetException( + new SocketException((int)args.SocketError)); + CleanupArgs(args); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.SendAsync" /> call, when sending a request packet to the + /// network. In this case, the <see cref="SocketAsyncEventArgs.ConnectSocket" /> will be used to perform the transmission. + /// </summary> + private void HandleSentRequest(SocketAsyncEventArgs args, WriterStateToken writerState) + { + TaskCompletionSource<int> tcs = writerState.RequestCompletionSource!; + + int sent = args.BytesTransferred; + int previouslySent = args.Offset; + int totalSent = previouslySent + sent; + int expected = writerState.BytesToTransfer; + + if (totalSent == expected) + { + tcs.SetResult(totalSent - RawPacketHeader.Length); + CleanupArgs(args); + } + else if (totalSent > 0 && totalSent < expected) + { + args.SetBuffer(totalSent, expected - totalSent); + StartOrContinueSending(args.ConnectSocket, args); + } + else if (sent == 0) + { + // connection is dead + tcs.SetException(new SocketException((int)SocketError.HostDown)); + CleanupArgs(args); + } + } + + /// <summary> + /// Starts or continues an asynchronous network read operation. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartOrContinueReceiving(SocketAsyncEventArgs args) + { + if (args.AcceptSocket.ReceiveAsync(args)) + { + return; + } + + HandleReceived(args); + } + + /// <summary> + /// Starts or continues an asynchronous network write operation using the given socket. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartOrContinueSending(Socket connection, SocketAsyncEventArgs args) + { + if (connection.SendAsync(args)) + { + return; + } + + HandleSent(args); + } + + private sealed class WriterStateToken : IDisposable + { + internal int BytesToTransfer { get; set; } + + internal Memory<byte> UserBuffer { get; set; } + + internal RawPacketHeader? Header { get; set; } + + internal TaskCompletionSource<int>? RequestCompletionSource { get; set; } + + /// <inheritdoc /> + public void Dispose() + { + Reset(); + } + + internal void Reset() + { + BytesToTransfer = 0; + UserBuffer = Memory<byte>.Empty; + Header = null; + RequestCompletionSource = null; + } + } + } +} diff --git a/NetSharp/NetSharp/Raw/Stream/RawStreamConnection.cs b/NetSharp/NetSharp/Raw/Stream/RawStreamConnection.cs @@ -1,931 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using NetSharp.Utils; - -namespace NetSharp.Raw.Stream -{ - /// <summary> - /// Handles a message received on a raw stream connection. - /// </summary> - /// <param name="remoteEndPoint"> - /// The remote endpoint from which the received message originated. - /// </param> - /// <param name="header"> - /// The header of the received message. - /// </param> - /// <param name="data"> - /// The data held by the received message. - /// </param> - /// <param name="writer"> - /// A reference to the network connection, to interact with the network further. - /// </param> - public delegate void RawStreamPacketHandler( - EndPoint remoteEndPoint, - in RawPacketHeader header, - in ReadOnlyMemory<byte> data, - IRawStreamWriter writer); - - /// <summary> - /// Represents a network connection using a stream-based protocol to interact over the network, that is capable of - /// sending raw bytes. - /// </summary> - public sealed class RawStreamConnection : RawConnectionBase, IRawStreamWriter, IRawStreamPacketHandler - { - private readonly object activeRemoteConnectionsLock = new object(); - private readonly SlimObjectPool<OperationStateToken> operationStatePool; - private readonly SlimObjectPool<ReaderStateToken> readerStatePool; - private readonly ConcurrentDictionary<int, RawStreamPacketHandler> registeredHandlers; - private readonly List<RemoteConnectionWrapper> remoteConnections; - private readonly object remoteConnectionsLock = new object(); - private readonly SlimObjectPool<WriterStateToken> writerStatePool; - - private int activeRemoteConnections; - - /// <summary> - /// Initialises a new instance of the <see cref="RawStreamConnection" /> class. - /// </summary> - /// <param name="connectionProtocolType"> - /// The protocol that the underlying network connection should use. - /// </param> - /// <param name="defaultRemoteEndPoint"> - /// The default remote endpoint that should be used for pending connections. - /// </param> - public RawStreamConnection(ProtocolType connectionProtocolType, EndPoint defaultRemoteEndPoint) - : base(SocketType.Stream, connectionProtocolType, defaultRemoteEndPoint) - { - activeRemoteConnections = 0; - remoteConnections = new List<RemoteConnectionWrapper>(); - - registeredHandlers = new ConcurrentDictionary<int, RawStreamPacketHandler>(); - - static OperationStateToken CreateOperationToken() - { - return new OperationStateToken(); - } - - static void ResetOperationToken(ref OperationStateToken instance) - { - instance.Reset(); - } - - static void DestroyOperationToken(OperationStateToken instance) - { - instance.Dispose(); - } - - operationStatePool = new SlimObjectPool<OperationStateToken>( - CreateOperationToken, - ResetOperationToken, - DestroyOperationToken); - - static ReaderStateToken CreateReaderToken() - { - return new ReaderStateToken(); - } - - static void ResetReaderToken(ref ReaderStateToken instance) - { - instance.Reset(); - } - - static void DestroyReaderToken(ReaderStateToken instance) - { - instance.Dispose(); - } - - readerStatePool = new SlimObjectPool<ReaderStateToken>( - CreateReaderToken, - ResetReaderToken, - DestroyReaderToken); - - static WriterStateToken CreateWriterToken() - { - return new WriterStateToken(); - } - - static void ResetWriterToken(ref WriterStateToken instance) - { - instance.Reset(); - } - - static void DestroyWriterToken(WriterStateToken instance) - { - instance.Dispose(); - } - - writerStatePool = new SlimObjectPool<WriterStateToken>( - CreateWriterToken, - ResetWriterToken, - DestroyWriterToken); - } - - /// <summary> - /// Connects asynchronously to the given remote network endpoint. - /// </summary> - /// <param name="remoteEndPoint"> - /// The endpoint of the remote network connection to which we want to connect. - /// </param> - /// <returns> - /// A <see cref="Task" /> object representing the asynchronous operation. - /// </returns> - public Task ConnectAsync(EndPoint remoteEndPoint) - { - TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); - SocketAsyncEventArgs socketArgs = RentSocketArgs(); - - OperationStateToken operationState = operationStatePool.Rent(); - operationState.OperationCompletionSource = tcs; - - socketArgs.UserToken = operationState; - - socketArgs.RemoteEndPoint = remoteEndPoint; - - if (Connection.ConnectAsync(socketArgs)) - { - return tcs.Task; - } - - CleanupArgs(socketArgs); - - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void DeregisterHandler(int id, RawStreamPacketHandler handler) - { - if (registeredHandlers.TryGetValue(id, out RawStreamPacketHandler previousMulticast)) - { - // ReSharper disable once DelegateSubtraction - RawStreamPacketHandler? newMulticast = previousMulticast - handler; - - if (newMulticast != default) - { - _ = registeredHandlers.TryUpdate(id, newMulticast, previousMulticast); - } - } - } - - /// <summary> - /// Disconnects asynchronously from the currently connected remote network connection. - /// </summary> - /// <param name="leaveConnectionReusable"> - /// Whether the underlying network connection should be left in a reusable state after this call completes. - /// </param> - /// <returns> - /// A <see cref="Task" /> object representing the asynchronous operation. - /// </returns> - public Task DisconnectAsync(bool leaveConnectionReusable = false) - { - TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); - SocketAsyncEventArgs socketArgs = RentSocketArgs(); - - OperationStateToken operationState = operationStatePool.Rent(); - operationState.OperationCompletionSource = tcs; - - socketArgs.UserToken = operationState; - - socketArgs.DisconnectReuseSocket = leaveConnectionReusable; - - if (Connection.DisconnectAsync(socketArgs)) - { - return tcs.Task; - } - - CleanupArgs(socketArgs); - - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void RegisterHandler(int id, RawStreamPacketHandler handler) - { - _ = registeredHandlers.AddOrUpdate(id, _ => handler, (_, multicast) => multicast + handler); - } - - /// <inheritdoc /> - public ValueTask<int> SendAsync(ushort type, ReadOnlyMemory<byte> buffer, SocketFlags flags = SocketFlags.None) - { - return DoSendAsync(Connection, type, buffer, flags); - } - - /// <inheritdoc /> - protected override void CreateSocketArgsHook(ref SocketAsyncEventArgs instance) - { - if (instance == default) - { - return; - } - - instance.Completed += HandleIoCompleted; - - base.CreateSocketArgsHook(ref instance); - } - - /// <inheritdoc /> - protected override void DestroySocketArgsHook(ref SocketAsyncEventArgs instance) - { - if (instance == default) - { - return; - } - - instance.Completed -= HandleIoCompleted; - - base.DestroySocketArgsHook(ref instance); - } - - /// <inheritdoc /> - protected override void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - if (disposing) - { - foreach (RemoteConnectionWrapper remoteConnection in remoteConnections) - { - remoteConnection.LocalShutdown(); - } - - lock (activeRemoteConnectionsLock) - { - while (activeRemoteConnections > 0) - { - _ = Monitor.Wait(activeRemoteConnectionsLock); - } - } - - operationStatePool.Dispose(); - readerStatePool.Dispose(); - writerStatePool.Dispose(); - } - - base.Dispose(disposing); - } - - /// <inheritdoc /> - protected override void HandlerTaskWork() - { - while (true) - { - SocketAsyncEventArgs socketArgs = RentSocketArgs(); - - if (Connection.AcceptAsync(socketArgs)) - { - // we want to stop starting accept operations when there are no more connections in the queue. when - // this happens Connection.AcceptAsync() will return "true", so we can break. - break; - } - - HandleAccepted(socketArgs); - } - } - - /// <inheritdoc /> - protected override void ResetSocketArgsHook(ref SocketAsyncEventArgs instance) - { - if (instance == default) - { - return; - } - - instance.AcceptSocket = null; - - base.ResetSocketArgsHook(ref instance); - } - - /// <inheritdoc /> - protected override void StartHook(int concurrentTasks) - { - Connection.Listen(concurrentTasks); - - base.StartHook(concurrentTasks); - } - - /// <summary> - /// Prepares the given socket args for sending a request to the network. - /// </summary> - private static void ConfigureSendRequestAsync( - SocketAsyncEventArgs args, - ref byte[] ownedBuffer, - in RawPacketHeader pendingHeader, - in ReadOnlyMemory<byte> pendingData, - WriterStateToken writerState, - TaskCompletionSource<int> tcs) - { - Memory<byte> ownedBufferMemory = new Memory<byte>(ownedBuffer); - RawPacket.Serialise(in ownedBufferMemory, in pendingHeader, in pendingData); - - int totalTransferredBytes = RawPacket.TotalSize(in pendingHeader); - args.SetBuffer(ownedBuffer, 0, totalTransferredBytes); - - writerState.BytesToTransfer = totalTransferredBytes; - writerState.RequestCompletionSource = tcs; - - args.UserToken = writerState; - } - - /// <summary> - /// Cleans up and returns the given socket args. - /// </summary> - private void CleanupArgs(SocketAsyncEventArgs args, bool cleanupUserToken = true) - { - if (cleanupUserToken) - { - switch (args.UserToken) - { - case OperationStateToken operationState: - operationStatePool.Return(operationState); - break; - - case ReaderStateToken readerState: - readerStatePool.Return(readerState); - break; - - case WriterStateToken writerState: - writerStatePool.Return(writerState); - break; - } - } - - ReturnSocketArgs(args); - } - - /// <summary> - /// Prepares the given socket args for receiving a packet's data from the network. - /// </summary> - private void ConfigureReceiveDataAsync(SocketAsyncEventArgs args, ReaderStateToken readerState, in RawPacketHeader header) - { - ReturnBuffer(args.Buffer); // return and clear the previously parsed request header buffer - - byte[] pendingDataBuffer = RentBuffer(header.DataLength); - - args.SetBuffer(pendingDataBuffer, 0, header.DataLength); - - readerState.BytesToTransfer = header.DataLength; - readerState.RequestHeader = header; - - args.UserToken = readerState; - } - - /// <summary> - /// Prepares the given socket args for receiving a packet's header from the network. - /// </summary> - private void ConfigureReceiveHeaderAsync(SocketAsyncEventArgs args, ReaderStateToken readerState) - { - ReturnBuffer(args.Buffer); // return and clear the previously sent response packet buffer - - byte[] pendingHeaderBuffer = RentBuffer(RawPacketHeader.Length); - - args.SetBuffer(pendingHeaderBuffer, 0, RawPacketHeader.Length); - - readerState.BytesToTransfer = RawPacketHeader.Length; - - args.UserToken = readerState; - } - - private ValueTask<int> DoSendAsync(Socket connection, ushort type, ReadOnlyMemory<byte> buffer, SocketFlags flags) - { - TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(); - SocketAsyncEventArgs socketArgs = RentSocketArgs(); - - RawPacketHeader header = new RawPacketHeader(type, buffer.Length); - byte[] ownedBuffer = RentBuffer(RawPacket.TotalSize(in header)); - - WriterStateToken writerState = writerStatePool.Rent(); - - ConfigureSendRequestAsync(socketArgs, ref ownedBuffer, in header, in buffer, writerState, tcs); - - socketArgs.SocketFlags = flags; - - StartOrContinueSending(connection, socketArgs); - - return new ValueTask<int>(tcs.Task); - } - - /// <summary> - /// Handles a completed <see cref="Socket.AcceptAsync" /> call. - /// </summary> - private void HandleAccepted(SocketAsyncEventArgs args) - { - switch (args.SocketError) - { - case SocketError.Success: - _ = Interlocked.Increment(ref activeRemoteConnections); - - // the buffer is set to allow a simpler ConfigureReceiveHeader() implementation. Since returning an - // empty buffer is ignored in the array pool, this allows us to just return the last assigned buffer - // in the ConfigureXXX() method to the pool (this means that usually we will usually be returning - // the ResponseDataBuffer). - args.SetBuffer(Array.Empty<byte>(), 0, 0); - - ReaderStateToken readerState = readerStatePool.Rent(); - - Socket remoteConnection = args.AcceptSocket; - RemoteConnectionWrapper wrapper = new RemoteConnectionWrapper(this, remoteConnection, readerState); - - lock (remoteConnectionsLock) - { - remoteConnections.Add(wrapper); - } - - WaitHandle[] eventHandles = - { - readerState.ConnectionClosed.WaitHandle, - readerState.RequestReceived.WaitHandle, - }; - - while (true) - { - ConfigureReceiveHeaderAsync(args, readerState); - StartOrContinueReceiving(args); - - int completedEvent = WaitHandle.WaitAny(eventHandles); - - if (completedEvent == 0) - { - // the connection has been closed - break; - } - - // since the request received event has been set, we need to reset it - readerState.RequestReceived.Reset(); - - RawPacketHeader header = readerState.RequestHeader!.Value; - - byte[] dataBuffer = args.Buffer; - ReadOnlyMemory<byte> dataBufferMemory = new ReadOnlyMemory<byte>(dataBuffer, 0, header.DataLength); - - if (registeredHandlers.TryGetValue(header.Type, out RawStreamPacketHandler handler)) - { - handler.Invoke(remoteConnection.RemoteEndPoint, in header, in dataBufferMemory, wrapper); - } - } - - lock (activeRemoteConnectionsLock) - { - _ = Interlocked.Decrement(ref activeRemoteConnections); - Monitor.Pulse(activeRemoteConnectionsLock); // signals that we may have reached the activeRemoteConnections == 0 state - } - - lock (remoteConnectionsLock) - { - // TODO: ensure that this is atomic via locking or something else. are properties inherently atomic??? - if (!readerState.LocalShutdownSignaled) - { - // since we were shutdown remotely, we need to remove the remote connection from the list of tracked connections - // TODO: come up with a better way of tracking active remote connections (have the wrapper deregister itself?) - _ = remoteConnections.Remove(wrapper); - - remoteConnection.Disconnect(false); - remoteConnection.Shutdown(SocketShutdown.Both); - remoteConnection.Close(); - remoteConnection.Dispose(); - } - } - - CleanupArgs(args); - break; - - default: - CleanupArgs(args, false); // there is no StateToken to cleanup - break; - } - } - - /// <summary> - /// Handles a completed <see cref="Socket.ConnectAsync(SocketAsyncEventArgs)" /> call. - /// </summary> - private void HandleConnected(SocketAsyncEventArgs args) - { - OperationStateToken readerState = (OperationStateToken)args.UserToken; - TaskCompletionSource<bool>? tcs = readerState.OperationCompletionSource; - - Debug.Assert( - tcs != default, - "HandleConnected was passed a state token without the correct TaskCompletionSource!"); - - switch (args.SocketError) - { - case SocketError.Success: - tcs.SetResult(true); - break; - - case SocketError.OperationAborted: - tcs.SetCanceled(); - break; - - default: - tcs.SetException(new SocketException((int)args.SocketError)); - break; - } - - CleanupArgs(args); - } - - /// <summary> - /// Handles a completed <see cref="Socket.DisconnectAsync" /> call. - /// </summary> - private void HandleDisconnected(SocketAsyncEventArgs args) - { - OperationStateToken readerState = (OperationStateToken)args.UserToken; - TaskCompletionSource<bool>? tcs = readerState.OperationCompletionSource; - - Debug.Assert( - tcs != default, - "HandleDisconnected was passed a state token without the correct TaskCompletionSource!"); - - switch (args.SocketError) - { - case SocketError.Success: - tcs.SetResult(true); - break; - - case SocketError.OperationAborted: - tcs.SetCanceled(); - break; - - default: - tcs.SetException(new SocketException((int)args.SocketError)); - break; - } - - CleanupArgs(args); - } - - /// <summary> - /// Handles the completion of an asynchronous socket operation. - /// </summary> - private void HandleIoCompleted(object sender, SocketAsyncEventArgs args) - { - switch (args.LastOperation) - { - case SocketAsyncOperation.Accept: - if (args.SocketError != SocketError.OperationAborted) - { - HandlerTaskWork(); - } - - HandleAccepted(args); - break; - - case SocketAsyncOperation.Connect: - HandleConnected(args); - break; - - case SocketAsyncOperation.Disconnect: - HandleDisconnected(args); - break; - - case SocketAsyncOperation.Receive: - HandleReceived(args); - break; - - case SocketAsyncOperation.Send: - HandleSent(args); - break; - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call. - /// </summary> - private void HandleReceived(SocketAsyncEventArgs args) - { - ReaderStateToken readerState = (ReaderStateToken)args.UserToken; - - switch (args.SocketError) - { - case SocketError.Success: - switch (readerState.BytesToTransfer) - { - case RawPacketHeader.Length: - HandleReceivedHeader(args, readerState); - break; - - default: - HandleReceivedData(args, readerState); - break; - } - - break; - - default: - readerState.ConnectionClosed.Set(); - break; - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's data from the network. - /// </summary> - private void HandleReceivedData(SocketAsyncEventArgs args, ReaderStateToken readerState) - { - int received = args.BytesTransferred; - int previouslyReceived = args.Offset; - int totalReceived = previouslyReceived + received; - int expected = readerState.BytesToTransfer; - - if (totalReceived == expected) - { - readerState.RequestReceived.Set(); - } - else if (totalReceived > 0 && totalReceived < expected) - { - args.SetBuffer(totalReceived, expected - totalReceived); - StartOrContinueReceiving(args); - } - else if (received == 0) - { - readerState.ConnectionClosed.Set(); - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's header from - /// the network. - /// </summary> - private void HandleReceivedHeader(SocketAsyncEventArgs args, ReaderStateToken readerState) - { - int received = args.BytesTransferred; - int previouslyReceived = args.Offset; - int totalReceived = previouslyReceived + received; - int expected = readerState.BytesToTransfer; - - byte[] headerBuffer = args.Buffer; - ReadOnlySpan<byte> headerBufferMemory = new ReadOnlySpan<byte>(headerBuffer); - - if (totalReceived == expected) - { - RawPacketHeader header = RawPacketHeader.Deserialise(in headerBufferMemory); - - ConfigureReceiveDataAsync(args, readerState, in header); - StartOrContinueReceiving(args); - } - else if (totalReceived > 0 && totalReceived < expected) - { - args.SetBuffer(totalReceived, expected - totalReceived); - StartOrContinueReceiving(args); - } - else if (received == 0) - { - readerState.ConnectionClosed.Set(); - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.SendAsync" /> call. - /// </summary> - private void HandleSent(SocketAsyncEventArgs args) - { - switch (args.UserToken) - { - case ReaderStateToken readerState: - switch (args.SocketError) - { - case SocketError.Success: - HandleSentResponse(args, readerState); - break; - - default: - readerState.ConnectionClosed.Set(); - break; - } - - break; - - case WriterStateToken writerState: - switch (args.SocketError) - { - case SocketError.Success: - HandleSentRequest(args, writerState); - break; - - case SocketError.OperationAborted: - writerState.RequestCompletionSource!.SetCanceled(); - CleanupArgs(args); - break; - - default: - writerState.RequestCompletionSource!.SetException( - new SocketException((int)args.SocketError)); - CleanupArgs(args); - break; - } - - break; - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.SendAsync" /> call, when sending a request packet to the - /// network. In this case, the <see cref="SocketAsyncEventArgs.ConnectSocket" /> will be used to perform the transmission. - /// </summary> - private void HandleSentRequest(SocketAsyncEventArgs args, WriterStateToken writerState) - { - TaskCompletionSource<int> tcs = writerState.RequestCompletionSource!; - - int sent = args.BytesTransferred; - int previouslySent = args.Offset; - int totalSent = previouslySent + sent; - int expected = writerState.BytesToTransfer; - - if (totalSent == expected) - { - tcs.SetResult(totalSent - RawPacketHeader.Length); - CleanupArgs(args); - } - else if (totalSent > 0 && totalSent < expected) - { - args.SetBuffer(totalSent, expected - totalSent); - StartOrContinueSending(args.ConnectSocket, args); - } - else if (sent == 0) - { - // connection is dead - tcs.SetException(new SocketException((int)SocketError.HostDown)); - CleanupArgs(args); - } - } - - /// <summary> - /// Handles the completion of a <see cref="Socket.SendAsync" /> call, when sending a response packet to the - /// network. In this case, the <see cref="SocketAsyncEventArgs.AcceptSocket" /> will be used to perform the transmission. - /// </summary> - private void HandleSentResponse(SocketAsyncEventArgs args, ReaderStateToken readerState) - { - int sent = args.BytesTransferred; - int previouslySent = args.Offset; - int totalSent = previouslySent + sent; - int expected = readerState.BytesToTransfer; - - if (totalSent == expected) - { - // TODO do we need to do any notification that a send has completed? - } - else if (totalSent > 0 && totalSent < expected) - { - args.SetBuffer(totalSent, expected - totalSent); - StartOrContinueSending(args.AcceptSocket, args); - } - else if (sent == 0) - { - readerState.ConnectionClosed.Set(); - } - } - - /// <summary> - /// Starts or continues an asynchronous network read operation. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void StartOrContinueReceiving(SocketAsyncEventArgs args) - { - if (args.AcceptSocket.ReceiveAsync(args)) - { - return; - } - - HandleReceived(args); - } - - /// <summary> - /// Starts or continues an asynchronous network write operation using the given socket. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void StartOrContinueSending(Socket connection, SocketAsyncEventArgs args) - { - if (connection.SendAsync(args)) - { - return; - } - - HandleSent(args); - } - - private sealed class OperationStateToken : IDisposable - { - /// <summary> - /// The <see cref="TaskCompletionSource{TResult}" /> for asynchronous network operations. - /// </summary> - internal TaskCompletionSource<bool>? OperationCompletionSource { get; set; } - - /// <inheritdoc /> - public void Dispose() - { - Reset(); - } - - internal void Reset() - { - OperationCompletionSource = null; - } - } - - private sealed class ReaderStateToken : IDisposable - { - internal int BytesToTransfer { get; set; } - - internal ManualResetEventSlim ConnectionClosed { get; } = new ManualResetEventSlim(false); - - internal bool LocalShutdownSignaled { get; set; } - - internal RawPacketHeader? RequestHeader { get; set; } - - internal ManualResetEventSlim RequestReceived { get; } = new ManualResetEventSlim(false); - - /// <inheritdoc /> - public void Dispose() - { - Reset(); - - ConnectionClosed.Dispose(); - RequestReceived.Dispose(); - } - - internal void Reset() - { - BytesToTransfer = 0; - RequestHeader = null; - - ConnectionClosed.Reset(); - RequestReceived.Reset(); - } - } - - private sealed class RemoteConnectionWrapper : IRawStreamWriter - { - private readonly WeakReference<Socket> connectionRef; - private readonly WeakReference<RawStreamConnection> parentRef; - private readonly WeakReference<ReaderStateToken> tokenRef; - - internal RemoteConnectionWrapper(RawStreamConnection parent, Socket connection, ReaderStateToken token) - { - parentRef = new WeakReference<RawStreamConnection>(parent); - connectionRef = new WeakReference<Socket>(connection); - tokenRef = new WeakReference<ReaderStateToken>(token); - } - - public void LocalShutdown() - { - bool connectionDisposed = !connectionRef.TryGetTarget(out Socket connection); - bool tokenDisposed = !tokenRef.TryGetTarget(out ReaderStateToken token); - - if (connectionDisposed || tokenDisposed) - { - return; - } - - token.LocalShutdownSignaled = true; - - connection.Disconnect(false); - connection.Shutdown(SocketShutdown.Both); - connection.Close(); - connection.Dispose(); - } - - /// <inheritdoc /> - public ValueTask<int> SendAsync(ushort type, ReadOnlyMemory<byte> buffer, SocketFlags flags = SocketFlags.None) - { - bool parentDisposed = !parentRef.TryGetTarget(out RawStreamConnection parent); - bool connectionDisposed = !connectionRef.TryGetTarget(out Socket connection); - - if (parentDisposed || connectionDisposed) - { - // TODO: just use a result of -1? some other error code? - throw new ObjectDisposedException(parentDisposed ? nameof(parent) : nameof(connection)); - } - - return parent.DoSendAsync(connection, type, buffer, flags); - } - } - - private sealed class WriterStateToken : IDisposable - { - internal int BytesToTransfer { get; set; } - - internal TaskCompletionSource<int>? RequestCompletionSource { get; set; } - - /// <inheritdoc /> - public void Dispose() - { - Reset(); - } - - internal void Reset() - { - BytesToTransfer = 0; - RequestCompletionSource = null; - } - } - } -} diff --git a/NetSharp/NetSharp/Raw/Stream/RawStreamServer.cs b/NetSharp/NetSharp/Raw/Stream/RawStreamServer.cs @@ -0,0 +1,604 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using NetSharp.Utils; + +namespace NetSharp.Raw.Stream +{ + /// <summary> + /// Represents a server using a stream-based protocol to interact with clients. Handles raw binary packets. + /// </summary> + public sealed class RawStreamServer : RawConnectionBase, IRawServer, IRawStreamPacketHandler + { + private readonly object activeRemoteConnectionsLock = new object(); + private readonly SlimObjectPool<ReaderStateToken> readerStatePool; + private readonly ConcurrentDictionary<uint, RawStreamPacketHandler> registeredHandlers; + private readonly List<RemoteConnectionWrapper> remoteConnections; + private readonly object remoteConnectionsLock = new object(); + + private int activeRemoteConnections; + + /// <summary> + /// Initialises a new instance of the <see cref="RawStreamServer" /> class. + /// </summary> + /// <param name="connectionProtocolType"> + /// The protocol that the underlying network connection should use. + /// </param> + /// <param name="defaultRemoteEndPoint"> + /// The default remote endpoint that should be used for pending connections. + /// </param> + public RawStreamServer(ProtocolType connectionProtocolType, EndPoint defaultRemoteEndPoint) + : base(SocketType.Stream, connectionProtocolType, defaultRemoteEndPoint) + { + activeRemoteConnections = 0; + remoteConnections = new List<RemoteConnectionWrapper>(); + + registeredHandlers = new ConcurrentDictionary<uint, RawStreamPacketHandler>(); + + static ReaderStateToken CreateReaderToken() + { + return new ReaderStateToken(); + } + + static void ResetReaderToken(ref ReaderStateToken instance) + { + instance.Reset(); + } + + static void DestroyReaderToken(ReaderStateToken instance) + { + instance.Dispose(); + } + + readerStatePool = new SlimObjectPool<ReaderStateToken>( + CreateReaderToken, + ResetReaderToken, + DestroyReaderToken); + } + + /// <inheritdoc /> + public void DeregisterHandler(uint id, RawStreamPacketHandler handler) + { + if (registeredHandlers.TryGetValue(id, out RawStreamPacketHandler previousMulticast)) + { + // ReSharper disable once DelegateSubtraction + RawStreamPacketHandler? newMulticast = previousMulticast - handler; + + if (newMulticast != default) + { + _ = registeredHandlers.TryUpdate(id, newMulticast, previousMulticast); + } + } + } + + /// <inheritdoc /> + public void RegisterHandler(uint id, RawStreamPacketHandler handler) + { + _ = registeredHandlers.AddOrUpdate(id, _ => handler, (_, multicast) => multicast + handler); + } + + /// <inheritdoc /> + public void Start(int concurrentTasks = 1) + { + Debug.Assert(IsBound, "Connection must be bound before we can start listening to the network!"); + + StartHook(concurrentTasks); + + int taskCount = concurrentTasks < 1 ? 1 : concurrentTasks; + + for (int i = 0; i < taskCount; i++) + { + AcceptAllPending(); + } + } + + /// <inheritdoc /> + protected override void CreateSocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.Completed += HandleIoCompleted; + + base.CreateSocketArgsHook(ref instance); + } + + /// <inheritdoc /> + protected override void DestroySocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.Completed -= HandleIoCompleted; + + base.DestroySocketArgsHook(ref instance); + } + + /// <inheritdoc /> + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + if (disposing) + { + foreach (RemoteConnectionWrapper remoteConnection in remoteConnections) + { + remoteConnection.LocalShutdown(); + } + + lock (activeRemoteConnectionsLock) + { + while (activeRemoteConnections > 0) + { + _ = Monitor.Wait(activeRemoteConnectionsLock); + } + } + + readerStatePool.Dispose(); + } + + base.Dispose(disposing); + } + + /// <inheritdoc /> + protected override void ResetSocketArgsHook(ref SocketAsyncEventArgs instance) + { + if (instance == default) + { + return; + } + + instance.AcceptSocket = null; + + base.ResetSocketArgsHook(ref instance); + } + + /// <inheritdoc /> + protected override void StartHook(int concurrentTasks) + { + Connection.Listen(concurrentTasks); + + base.StartHook(concurrentTasks); + } + + private void AcceptAllPending() + { + while (true) + { + SocketAsyncEventArgs socketArgs = RentSocketArgs(); + + if (Connection.AcceptAsync(socketArgs)) + { + // we want to stop starting accept operations when there are no more connections in the queue. when + // this happens Connection.AcceptAsync() will return "true", so we can break. + break; + } + + HandleAccepted(socketArgs); + } + } + + /// <summary> + /// Cleans up and returns the given socket args. + /// </summary> + private void CleanupArgs(SocketAsyncEventArgs args, bool cleanupUserToken = true) + { + ReaderStateToken readerState = (ReaderStateToken)args.UserToken; + + if (cleanupUserToken) + { + readerStatePool.Return(readerState); + } + + ReturnSocketArgs(args); + } + + /// <summary> + /// Prepares the given socket args for receiving a packet's data from the network. + /// </summary> + private void ConfigureReceiveDataAsync(SocketAsyncEventArgs args, ReaderStateToken readerState, in RawPacketHeader header) + { + ReturnBuffer(args.Buffer); // return and clear the previously parsed request header buffer + + byte[] pendingDataBuffer = RentBuffer(header.DataLength); + + args.SetBuffer(pendingDataBuffer, 0, header.DataLength); + + readerState.BytesToTransfer = header.DataLength; + readerState.RequestHeader = header; + + args.UserToken = readerState; + } + + /// <summary> + /// Prepares the given socket args for receiving a packet's header from the network. + /// </summary> + private void ConfigureReceiveHeaderAsync(SocketAsyncEventArgs args, ReaderStateToken readerState) + { + ReturnBuffer(args.Buffer); // return and clear the previously sent response packet buffer + + byte[] pendingHeaderBuffer = RentBuffer(RawPacketHeader.Length); + + args.SetBuffer(pendingHeaderBuffer, 0, RawPacketHeader.Length); + + readerState.BytesToTransfer = RawPacketHeader.Length; + + args.UserToken = readerState; + } + + /// <summary> + /// Handles a completed <see cref="Socket.AcceptAsync" /> call. + /// </summary> + private void HandleAccepted(SocketAsyncEventArgs args) + { + switch (args.SocketError) + { + case SocketError.Success: + _ = Interlocked.Increment(ref activeRemoteConnections); + + // the buffer is set to allow a simpler ConfigureReceiveHeader() implementation. Since returning an + // empty buffer is ignored in the array pool, this allows us to just return the last assigned buffer + // in the ConfigureXXX() method to the pool (this means that usually we will usually be returning + // the ResponseDataBuffer). + args.SetBuffer(Array.Empty<byte>(), 0, 0); + + ReaderStateToken readerState = readerStatePool.Rent(); + + Socket remoteConnection = args.AcceptSocket; + RemoteConnectionWrapper wrapper = new RemoteConnectionWrapper(this, remoteConnection, readerState); + + lock (remoteConnectionsLock) + { + remoteConnections.Add(wrapper); + } + + WaitHandle[] eventHandles = + { + readerState.ConnectionClosed.WaitHandle, + readerState.RequestReceived.WaitHandle, + }; + + while (true) + { + ConfigureReceiveHeaderAsync(args, readerState); + StartOrContinueReceiving(args); + + int completedEvent = WaitHandle.WaitAny(eventHandles); + + if (completedEvent == 0) + { + // the connection has been closed + break; + } + + // since the request received event has been set, we need to reset it + readerState.RequestReceived.Reset(); + + RawPacketHeader header = readerState.RequestHeader!.Value; + + byte[] dataBuffer = args.Buffer; + ReadOnlyMemory<byte> dataBufferMemory = new ReadOnlyMemory<byte>(dataBuffer, 0, header.DataLength); + + if (registeredHandlers.TryGetValue(header.Type, out RawStreamPacketHandler handler)) + { + handler.Invoke(remoteConnection.RemoteEndPoint, in header, in dataBufferMemory, wrapper); + } + } + + lock (activeRemoteConnectionsLock) + { + _ = Interlocked.Decrement(ref activeRemoteConnections); + Monitor.Pulse(activeRemoteConnectionsLock); // signals that we may have reached the activeRemoteConnections == 0 state + } + + lock (remoteConnectionsLock) + { + // TODO: ensure that this is atomic via locking or something else. are properties of integral types inherently atomic??? + if (!readerState.LocalShutdownSignaled) + { + // since we were shutdown remotely, we need to remove the remote connection from the list of tracked connections + // TODO: come up with a better way of tracking active remote connections (have the wrapper deregister itself?) + _ = remoteConnections.Remove(wrapper); + + remoteConnection.Disconnect(false); + remoteConnection.Shutdown(SocketShutdown.Both); + remoteConnection.Close(); + remoteConnection.Dispose(); + } + } + + CleanupArgs(args); + break; + + default: + CleanupArgs(args, false); // there is no StateToken to cleanup + break; + } + } + + /// <summary> + /// Handles the completion of an asynchronous socket operation. + /// </summary> + private void HandleIoCompleted(object sender, SocketAsyncEventArgs args) + { + switch (args.LastOperation) + { + case SocketAsyncOperation.Accept: + if (args.SocketError != SocketError.OperationAborted) + { + AcceptAllPending(); + } + + HandleAccepted(args); + break; + + case SocketAsyncOperation.Receive: + HandleReceived(args); + break; + + case SocketAsyncOperation.Send: + HandleSent(args); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call. + /// </summary> + private void HandleReceived(SocketAsyncEventArgs args) + { + ReaderStateToken readerState = (ReaderStateToken)args.UserToken; + + switch (args.SocketError) + { + case SocketError.Success: + switch (readerState.BytesToTransfer) + { + case RawPacketHeader.Length: + HandleReceivedHeader(args, readerState); + break; + + default: + HandleReceivedData(args, readerState); + break; + } + + break; + + default: + readerState.ConnectionClosed.Set(); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's data from the network. + /// </summary> + private void HandleReceivedData(SocketAsyncEventArgs args, ReaderStateToken readerState) + { + int received = args.BytesTransferred; + int previouslyReceived = args.Offset; + int totalReceived = previouslyReceived + received; + int expected = readerState.BytesToTransfer; + + if (totalReceived == expected) + { + readerState.RequestReceived.Set(); + } + else if (totalReceived > 0 && totalReceived < expected) + { + args.SetBuffer(totalReceived, expected - totalReceived); + StartOrContinueReceiving(args); + } + else if (received == 0) + { + readerState.ConnectionClosed.Set(); + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.ReceiveAsync" /> call, when receiving a packet's header from + /// the network. + /// </summary> + private void HandleReceivedHeader(SocketAsyncEventArgs args, ReaderStateToken readerState) + { + int received = args.BytesTransferred; + int previouslyReceived = args.Offset; + int totalReceived = previouslyReceived + received; + int expected = readerState.BytesToTransfer; + + byte[] headerBuffer = args.Buffer; + ReadOnlySpan<byte> headerBufferMemory = new ReadOnlySpan<byte>(headerBuffer); + + if (totalReceived == expected) + { + RawPacketHeader header = RawPacketHeader.Deserialise(in headerBufferMemory); + + ConfigureReceiveDataAsync(args, readerState, in header); + StartOrContinueReceiving(args); + } + else if (totalReceived > 0 && totalReceived < expected) + { + args.SetBuffer(totalReceived, expected - totalReceived); + StartOrContinueReceiving(args); + } + else if (received == 0) + { + readerState.ConnectionClosed.Set(); + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.SendAsync" /> call. + /// </summary> + private void HandleSent(SocketAsyncEventArgs args) + { + ReaderStateToken readerState = (ReaderStateToken)args.UserToken; + switch (args.UserToken) + { + case SocketError.Success: + HandleSentResponse(args, readerState); + break; + + default: + readerState.ConnectionClosed.Set(); + break; + } + } + + /// <summary> + /// Handles the completion of a <see cref="Socket.SendAsync" /> call, when sending a response packet to the + /// network. In this case, the <see cref="SocketAsyncEventArgs.AcceptSocket" /> will be used to perform the transmission. + /// </summary> + private void HandleSentResponse(SocketAsyncEventArgs args, ReaderStateToken readerState) + { + int sent = args.BytesTransferred; + int previouslySent = args.Offset; + int totalSent = previouslySent + sent; + int expected = readerState.BytesToTransfer; + + if (totalSent > 0 && totalSent < expected) + { + args.SetBuffer(totalSent, expected - totalSent); + StartOrContinueSending(args.AcceptSocket, args); + } + else if (sent == 0) + { + readerState.ConnectionClosed.Set(); + } + } + + /// <summary> + /// Starts or continues an asynchronous network read operation. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartOrContinueReceiving(SocketAsyncEventArgs args) + { + if (args.AcceptSocket.ReceiveAsync(args)) + { + return; + } + + HandleReceived(args); + } + + /// <summary> + /// Starts or continues an asynchronous network write operation using the given socket. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartOrContinueSending(Socket connection, SocketAsyncEventArgs args) + { + if (connection.SendAsync(args)) + { + return; + } + + HandleSent(args); + } + + private sealed class ReaderStateToken : IDisposable + { + internal int BytesToTransfer { get; set; } + + internal ManualResetEventSlim ConnectionClosed { get; } = new ManualResetEventSlim(false); + + internal bool LocalShutdownSignaled { get; set; } + + internal RawPacketHeader? RequestHeader { get; set; } + + internal ManualResetEventSlim RequestReceived { get; } = new ManualResetEventSlim(false); + + /// <inheritdoc /> + public void Dispose() + { + Reset(); + + ConnectionClosed.Dispose(); + RequestReceived.Dispose(); + } + + internal void Reset() + { + BytesToTransfer = 0; + RequestHeader = null; + + ConnectionClosed.Reset(); + RequestReceived.Reset(); + } + } + + private sealed class RemoteConnectionWrapper : IRawStreamClient + { + private readonly WeakReference<Socket> connectionRef; + private readonly WeakReference<RawStreamServer> parentRef; + private readonly WeakReference<ReaderStateToken> tokenRef; + + internal RemoteConnectionWrapper(RawStreamServer parent, Socket connection, ReaderStateToken token) + { + parentRef = new WeakReference<RawStreamServer>(parent); + connectionRef = new WeakReference<Socket>(connection); + tokenRef = new WeakReference<ReaderStateToken>(token); + } + + public void LocalShutdown() + { + bool connectionDisposed = !connectionRef.TryGetTarget(out Socket connection); + bool tokenDisposed = !tokenRef.TryGetTarget(out ReaderStateToken token); + + if (connectionDisposed || tokenDisposed) + { + return; + } + + token.LocalShutdownSignaled = true; + + connection.Disconnect(false); + connection.Shutdown(SocketShutdown.Both); + connection.Close(); + connection.Dispose(); + } + + /// <inheritdoc /> + public ValueTask<RawStreamReadAsyncResult> ReceiveAsync(Memory<byte> buffer, SocketFlags flags = SocketFlags.None) + { + bool parentDisposed = !parentRef.TryGetTarget(out RawStreamServer parent); + bool connectionDisposed = !connectionRef.TryGetTarget(out Socket connection); + + if (parentDisposed || connectionDisposed) + { + return new ValueTask<RawStreamReadAsyncResult>(new RawStreamReadAsyncResult(-1, 0)); + } + + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public ValueTask<RawStreamWriteAsyncResult> SendAsync(uint type, ReadOnlyMemory<byte> buffer, SocketFlags flags) + { + bool parentDisposed = !parentRef.TryGetTarget(out RawStreamServer parent); + bool connectionDisposed = !connectionRef.TryGetTarget(out Socket connection); + + if (parentDisposed || connectionDisposed) + { + return new ValueTask<RawStreamWriteAsyncResult>(new RawStreamWriteAsyncResult(-1)); + } + + throw new NotImplementedException(); + } + } + } +}