From: JamJar00 Date: Wed, 8 Jun 2016 20:58:30 +0000 (+0100) Subject: Various additions X-Git-Tag: 1.0.0~148 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=5acdf3f768c69f809b34b51e77ae0c522eee299a;p=rhonda%2Fimpostor.hazel.git Various additions --- diff --git a/Hazel.UnitTests/TcpConnectionTests.cs b/Hazel.UnitTests/TcpConnectionTests.cs index 66d9958..ccdf314 100644 --- a/Hazel.UnitTests/TcpConnectionTests.cs +++ b/Hazel.UnitTests/TcpConnectionTests.cs @@ -3,6 +3,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Threading; +using Hazel.Tcp; + namespace Hazel.UnitTests { [TestClass] @@ -14,13 +16,14 @@ namespace Hazel.UnitTests [TestMethod] public void TcpFieldTest() { + NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); + using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(ep)) { listener.Start(); - NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); - connection.Connect(ep); + connection.Connect(); //Connection fields Assert.AreEqual(ep, connection.EndPoint); @@ -39,11 +42,11 @@ namespace Hazel.UnitTests public void TcpIPv4ConnectionTest() { using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296, IPMode.IPv4)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4))) { listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4)); + connection.Connect(); } } @@ -57,14 +60,14 @@ namespace Hazel.UnitTests { listener.Start(); - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4))) { - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4)); + connection.Connect(); } - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4AndIPv6))) { - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4AndIPv6)); + connection.Connect(); } } } @@ -76,7 +79,7 @@ namespace Hazel.UnitTests public void TcpServerToClientTest() { using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunServerToClientTest(listener, connection, 4, 0, SendOption.FragmentedReliable); } @@ -89,7 +92,7 @@ namespace Hazel.UnitTests public void TcpClientToServerTest() { using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunClientToServerTest(listener, connection, 4, 0, SendOption.FragmentedReliable); } @@ -102,7 +105,7 @@ namespace Hazel.UnitTests public void ClientDisconnectTest() { using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunClientDisconnectTest(listener, connection); } @@ -115,7 +118,7 @@ namespace Hazel.UnitTests public void ServerDisconnectTest() { using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) - using (TcpConnection connection = new TcpConnection()) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunServerDisconnectTest(listener, connection); } diff --git a/Hazel.UnitTests/TestHelper.cs b/Hazel.UnitTests/TestHelper.cs index 7434fbd..7334be3 100644 --- a/Hazel.UnitTests/TestHelper.cs +++ b/Hazel.UnitTests/TestHelper.cs @@ -37,7 +37,7 @@ namespace Hazel.UnitTests listener.Start(); //Setup conneciton - connection.DataReceived += delegate(object sender, DataEventArgs args) + connection.DataReceived += delegate(object sender, DataReceivedEventArgs args) { Trace.WriteLine("Data was received correctly."); @@ -51,7 +51,7 @@ namespace Hazel.UnitTests mutex.Set(); }; - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); //Wait until data is received mutex.WaitOne(); @@ -76,7 +76,7 @@ namespace Hazel.UnitTests //Setup listener listener.NewConnection += delegate(object sender, NewConnectionEventArgs args) { - args.Connection.DataReceived += delegate(object innerSender, DataEventArgs innerArgs) + args.Connection.DataReceived += delegate(object innerSender, DataReceivedEventArgs innerArgs) { Trace.WriteLine("Data was received correctly."); @@ -99,7 +99,7 @@ namespace Hazel.UnitTests listener.Start(); //Connect - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); connection.SendBytes(data, sendOption); //Wait until data is received @@ -132,7 +132,7 @@ namespace Hazel.UnitTests listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); mutex.WaitOne(); } @@ -156,7 +156,7 @@ namespace Hazel.UnitTests listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); connection.Close(); diff --git a/Hazel.UnitTests/UdpConnectionTests.cs b/Hazel.UnitTests/UdpConnectionTests.cs index b93e6fb..363fb4d 100644 --- a/Hazel.UnitTests/UdpConnectionTests.cs +++ b/Hazel.UnitTests/UdpConnectionTests.cs @@ -3,6 +3,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Threading; +using Hazel.Udp; + namespace Hazel.UnitTests { [TestClass] @@ -14,13 +16,14 @@ namespace Hazel.UnitTests [TestMethod] public void UdpFieldTest() { + NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); + using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(ep)) { listener.Start(); - NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); - connection.Connect(ep); + connection.Connect(); //Connection fields Assert.AreEqual(ep, connection.EndPoint); @@ -39,11 +42,11 @@ namespace Hazel.UnitTests public void UdpIPv4ConnectionTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296, IPMode.IPv4)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4))) { listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4)); + connection.Connect(); } } @@ -57,14 +60,14 @@ namespace Hazel.UnitTests { listener.Start(); - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4))) { - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4)); + connection.Connect(); } - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4AndIPv6))) { - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4AndIPv6)); + connection.Connect(); } } } @@ -76,7 +79,7 @@ namespace Hazel.UnitTests public void UdpUnreliableServerToClientTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunServerToClientTest(listener, connection, 1, 3, SendOption.None); } @@ -89,7 +92,7 @@ namespace Hazel.UnitTests public void UdpReliableServerToClientTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunServerToClientTest(listener, connection, 3, 3, SendOption.Reliable); } @@ -102,7 +105,7 @@ namespace Hazel.UnitTests public void UdpUnreliableClientToServerTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunClientToServerTest(listener, connection, 1, 3, SendOption.None); } @@ -115,7 +118,7 @@ namespace Hazel.UnitTests public void UdpReliableClientToServerTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunClientToServerTest(listener, connection, 3, 3, SendOption.Reliable); } @@ -128,11 +131,11 @@ namespace Hazel.UnitTests public void KeepAliveClientTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); connection.KeepAliveInterval = 100; System.Threading.Thread.Sleep(1100); //Enough time for ~10 keep alive packets @@ -154,7 +157,7 @@ namespace Hazel.UnitTests ManualResetEvent mutex = new ManualResetEvent(false); using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { listener.NewConnection += delegate(object sender, NewConnectionEventArgs args) { @@ -173,7 +176,7 @@ namespace Hazel.UnitTests listener.Start(); - connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + connection.Connect(); mutex.WaitOne(); } @@ -186,7 +189,7 @@ namespace Hazel.UnitTests public void ClientDisconnectTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunClientDisconnectTest(listener, connection); } @@ -199,7 +202,7 @@ namespace Hazel.UnitTests public void ServerDisconnectTest() { using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) - using (UdpConnection connection = new UdpClientConnection()) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { TestHelper.RunServerDisconnectTest(listener, connection); } diff --git a/Hazel/Connection.cs b/Hazel/Connection.cs index 1f67312..c235f7e 100644 --- a/Hazel/Connection.cs +++ b/Hazel/Connection.cs @@ -40,15 +40,15 @@ namespace Hazel /// /// /// DataReceived is invoked everytime a message is received from the end point of this connection, the message - /// that was received can be found in the alongside other information from the + /// that was received can be found in the alongside other information from the /// event. /// /// /// /// - /// + /// /// - public event EventHandler DataReceived; + public event EventHandler DataReceived; /// /// Called when the end point disconnects or an error occurs. @@ -62,7 +62,7 @@ namespace Hazel /// /// /// - /// + /// /// public event EventHandler Disconnected; @@ -163,10 +163,10 @@ namespace Hazel /// /// /// Calling Connect makes the connection attempt to connect to the end point that's specified in the - /// passed. This method will block until the connection attempt completes and - /// will throw a if there is a problem connecting. + /// constructor. This method will block until the connection attempt completes and will throw a + /// if there is a problem connecting. /// - public abstract void Connect(ConnectionEndPoint remoteEndPoint); + public abstract void Connect(); /// /// Invokes the DataReceived event. @@ -180,11 +180,11 @@ namespace Hazel /// protected void InvokeDataReceived(byte[] bytes, SendOption sendOption) { - DataEventArgs args = DataEventArgs.GetObject(); + DataReceivedEventArgs args = DataReceivedEventArgs.GetObject(); args.Set(bytes, sendOption); //Make a copy to avoid race condition between null check and invocation - EventHandler handler = DataReceived; + EventHandler handler = DataReceived; if (handler != null) handler(this, args); } @@ -231,7 +231,7 @@ namespace Hazel /// connection. /// /// - /// This calls and therefore sets straight to + /// This calls and therefore sets straight to /// . Once you call Close you will not be able to send any more /// data using this connection and no more data will be received. /// diff --git a/Hazel/ConnectionListener.cs b/Hazel/ConnectionListener.cs index 8c4e165..4cd01a8 100644 --- a/Hazel/ConnectionListener.cs +++ b/Hazel/ConnectionListener.cs @@ -38,13 +38,13 @@ namespace Hazel /// /// Hazel doesn't store connections so it is your responsibility to keep track of the connections to your /// server. Note that as implements if you are not storing - /// a connection then as a bare minimum you should call here in order to + /// a connection then as a bare minimum you should call here in order to /// release the connection correctly. /// /// /// /// - /// + /// /// public event EventHandler NewConnection; @@ -57,18 +57,18 @@ namespace Hazel /// connects the event will be invoked containing the connection to the new client. /// /// - /// To stop listening you should call . + /// To stop listening you should call . /// /// /// - /// + /// /// public abstract void Start(); /// /// Invokes the NewConnection event with the supplied connection. /// - /// The connection to pass to subscribers. + /// The connection to pass in the arguments. /// /// Implementers should call this to invoke the event before data is received so that /// subscribers do not miss any data that may have been sent immediately after connecting. @@ -85,6 +85,18 @@ namespace Hazel handler(this, args); } + /// + /// Closes the connection listener safely. + /// + /// + /// Internally this simply calls Dispose therefore trying to reuse the ConnectionListener after calling Close will + /// cause ObjectDisposedExceptions. + /// + public virtual void Close() + { + Dispose(); + } + /// /// Call to dispose of the connection listener. /// diff --git a/Hazel/DataEventArgs.cs b/Hazel/DataEventArgs.cs index 1dd3c26..ef86cf9 100644 --- a/Hazel/DataEventArgs.cs +++ b/Hazel/DataEventArgs.cs @@ -6,28 +6,28 @@ using System.Text; namespace Hazel { /// - /// Event arguments for the event. + /// Event arguments for the event. /// /// /// /// This contains information about messages received by a connection and is passed to subscribers of the - /// DataEvent. + /// DataEvent. /// /// /// /// - public class DataEventArgs : EventArgs, IRecyclable + public class DataReceivedEventArgs : EventArgs, IRecyclable { /// /// Object pool for this event. /// - static readonly ObjectPool objectPool = new ObjectPool(() => new DataEventArgs()); + static readonly ObjectPool objectPool = new ObjectPool(() => new DataReceivedEventArgs()); /// /// Returns an instance of this object from the pool. /// /// A new or recycled DataEventArgs object. - internal static DataEventArgs GetObject() + internal static DataReceivedEventArgs GetObject() { return objectPool.GetObject(); } @@ -45,7 +45,7 @@ namespace Hazel /// /// Private constructor for object pool. /// - DataEventArgs() + DataReceivedEventArgs() { } diff --git a/Hazel/DisconnectedEventArgs.cs b/Hazel/DisconnectedEventArgs.cs index 1942244..21e29f5 100644 --- a/Hazel/DisconnectedEventArgs.cs +++ b/Hazel/DisconnectedEventArgs.cs @@ -36,9 +36,10 @@ namespace Hazel /// The exception, if any, that caused the disconnect. /// /// - /// If the disconnection was caused because of an exception occuring (for exemple a - /// on network based connections) this will contain the error that caused it or a - /// with the details of the exception, if the disconnection wasn't caused by an error then this will contain null. + /// If the disconnection was caused because of an exception occuring (for exemple a + /// on network based connections) this will contain the error + /// that caused it or a with the details of the exception, if the disconnection + /// wasn't caused by an error then this will contain null. /// public Exception Exception { get; private set; } diff --git a/Hazel/DocInclude/common.xml b/Hazel/DocInclude/common.xml index 76edc64..5e9ab9e 100644 --- a/Hazel/DocInclude/common.xml +++ b/Hazel/DocInclude/common.xml @@ -3,12 +3,12 @@ - As with all Hazel events it is invoked on a thread from the .NET and hence any - subscribers should ensure their handling code is thread safe. Implementing connections are not bound to - invoking this event in the sequence messages are received, in fact implementers are only required to - ensure this method is always and only invoked for a user sent message, therefore subscribers should be - aware that this event may be called out of order and may be called whilst another thread is still handling - an invocation of the event. + As with all Hazel events it is invoked on a thread from the .NET + ThreadPool and hence any subscribers should ensure their + handling code is thread safe. Implementing connections are not bound to invoking this event in the sequence + messages are received, in fact implementers are only required to ensure this method is always and only invoked + for a user sent message, therefore subscribers should be aware that this event may be called out of order and + may be called whilst another thread is still handling an invocation of the event. @@ -25,7 +25,7 @@ This method sends a number of bytes in a message to the end point of this client using the given to describe how the data should be sent. Sending messages requires that the this connection is connected to a remote end point and SendBytes will throw an exception if that is not - the case. See the property for information on whether a connection is connected or not. + the case. See the property for information on whether a connection is connected or not. \ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Content Layout.content b/Hazel/Documentation/Documentation/Content Layout.content new file mode 100644 index 0000000..e69b068 --- /dev/null +++ b/Hazel/Documentation/Documentation/Content Layout.content @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Introduction.aml b/Hazel/Documentation/Documentation/Introduction.aml new file mode 100644 index 0000000..f7a891e --- /dev/null +++ b/Hazel/Documentation/Documentation/Introduction.aml @@ -0,0 +1,24 @@ + + + + + + + Welcome to the Hazel documentation! Here you will find technical + details, API references and tutorials for getting started with Hazel. + + + Hazel Networking is an open source low level networking library for C# providing + connection orientated, message bassed communication via TCP, UDP and + RUDP. You can download it from Github + + here + https://github.com/DarkRiftNetworking/Hazel-Networking + _blank + + . + + + + + \ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Quickstart.aml b/Hazel/Documentation/Documentation/Quickstart.aml new file mode 100644 index 0000000..6cc3943 --- /dev/null +++ b/Hazel/Documentation/Documentation/Quickstart.aml @@ -0,0 +1,337 @@ + + + + + + + Hazel is a low level networking library that takes away a lot of the pain of writing sockets. Hazel provides the guarantee of connection orientated, message based communication across TCP, UDP and RUDP. + + + This guide will take you through the stages of writing a console based server and connecting to it from a console based client. + + + + + +
+ Creating a Server + + + Creating a Listener + + + + Create a new solution containing a Console project named "Server" (or at least something obvious). + + + + + Add a reference to Hazel.dll in the project. + + + + + Modify the default file to look like this, we'll go through each part individually. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; + +using Hazel; +using Hazel.Tcp; + +namespace HazelExample +{ + class ServerExample + { + static ConnectionListener listener; + + public static void Main(string[] args) + { + listener = new TcpConnectionListener(IPAddress.Any, 4296); + + listener.NewConnection += NewConnectionHandler; + + Console.WriteLine("Starting server!"); + + listener.Start(); + + Console.WriteLine("Press any key to continue..."); + + Console.ReadKey(); + + listener.Close(); + } + } +} + + + Firstly we need to tell the compiler that we are using Hazel; and using Hazel.Tcp; so we have access to Hazel's general and TCP specific types. Secondly we create a T:Hazel.ConnectionListener, these wait on a specified port and accept new clients to the server invoking the E:Hazel.ConnectionListener.NewConnection event each time a new client connects. + + + We then instruct the ConnectionListener to begin listening for new clients by calling M:ConnectionListener.Start and then finally we close the listener using M:ConnectionListener.Close. + + + In this circumstance it would be much better if we enclosed the listener within a using block as it implements IDisposable, however for the majority of use cases you are more likely to use the listener in this way. If you want to see it used in a using block then look at the unit tests. + + + If you want to use UDP instead of TCP then it is as simple as including the Hazel.Udp namespace and creating a T:UdpConnectionListener instead. + + + + + + + + Handling Events + + + + In the last example we subscribed to the E:ConnectionListener.NewConnection event but we never spsecified what to do in that event. Add the following method to your code. + +static void NewConnectionHandler(object sender, NewConnectionEventArgs args) +{ + Console.WriteLine("New connection from " + args.Connection.EndPoint.ToString(); + + args.Connection.DataReceived += DataReceivedHandler; +} + + This method is fairly simple, it follows the standard event handler delegate and take a sender (in this case it will be the ConnectionListener that called the event) and some args. The args contain a Connection which is the main object we use for communication with clients. + You can imagine this process in a similar way to TCP. You create a listener which creates sockets on the server side for each client socket that connects to it. + In this method we simply print out the IP of the client that just connected and then we subscribe to any data that this client sends us. You may also want to store the connection here for later reference as Hazel doesn't maintain a list of connection for you. + + + + + Once again we've got an undeclared event handler so lets fill that in now. + +private static void DataReceivedHandler(object sender, DataEventArgs args) +{ + Connection connection = (Connection)sender; + + Console.WriteLine("Received (" + string.Join<byte>(", ", args.Bytes) + ") from " + connection.EndPoint.ToString()); + + connection.SendBytes(args.Bytes, args.SendOption); + + args.Recycle(); +} + + Again this method follows the standard event handler delegate and this time we make use of the sender parameter to get the connection that received the data. We then go on to print out the data received and then we send it back to the client using M:ConnectionListener.SendBytes. When we send we specify that the send option should be the same as the data that was received, we'll talk more about send options later. + You have also probably noticed that I didn't mention the M:DataEventArgs.Recycle call. Recycle is an optional call that tells Hazel that it is now safe to use the object again rather than creating a new one and having to wait for the GC to collect the old object. If you are sending a lot of data then you will get less GC runs if you call Recycle but it is not necerssary to call it and if you dont the GC will collect it as normal. Also note that you should only call Recycle once you are done using the object otherwise the data inside it may change! + + + + + + In total, you should have something like this. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; + +using Hazel; +using Hazel.Tcp; + +namespace HazelExample +{ + class ServerExample + { + static ConnectionListener listener; + + public static void Main(string[] args) + { + listener = new TcpConnectionListener(IPAddress.Any, 4296); + + listener.NewConnection += NewConnectionHandler; + + Console.WriteLine("Starting server!"); + + listener.Start(); + + Console.WriteLine("Press any key to continue..."); + + Console.ReadKey(); + + listener.Close(); + } + + static void NewConnectionHandler(object sender, NewConnectionEventArgs args) + { + Console.WriteLine("New connection from " + args.Connection.EndPoint.ToString(); + + args.Connection.DataReceived += DataReceivedHandler; + + args.Recycle(); + } + + private static void DataReceivedHandler(object sender, DataEventArgs args) + { + Connection connection = (Connection)sender; + + Console.WriteLine("Received (" + string.Join<byte>(", ", args.Bytes) + ") from " + connection.EndPoint.ToString()); + + connection.SendBytes(args.Bytes, args.SendOption); + + args.Recycle(); + } + } +} + + +
+ +
+ Creating a Client + + + Connecting to a Server + + + + Create a new project in your solution for th client and add a reference to Hazel.dll. + + + + + + Modify the default file to contain the following. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Hazel; +using Hazel.Tcp; + +namespace HazelExample +{ + class ClientExample + { + static Connection connection; + + public static void Main(string[] args) + { + NetworkEndPoint endPoint = new NetworkEndPoint("127.0.0.1", 4296); + + connection = new TcpConnection(endPoint); + + connection.DataReceived += DataReceived; + + Console.WriteLine("Connecting!"); + + connection.Connect(); + + Console.WriteLine("Press any key to continue..."); + + Console.ReadKey(); + + connection.Close(); + } + } +} + + As you can see you simply create a T:NetworkEndPoint for the remote server and then pass it into a new T:Connection. Then you can setup any events needed and finally call M:Connection.Connect to begin the connection. + Finally we close the connection using M:Connection.Close. Again, Connection implements IDisposable and so must be closed, if it is easier you could wrap it in a using block. + + If you are using UDP instead of TCP then include the Hazel.Udp namespace and create a T:UdpClientConnection instead. + + + + + + + + Handling Events + + + + Add the following event handler to receive data, you'll notice that this is the same event we used in the server and so it will not be explained. + +private static void DataReceived(object sender, DataEventArgs args) +{ + Console.WriteLine("Received (" + string.Join<byte>(", ", args.Bytes) + ") from " + connection.EndPoint.ToString()); + + args.Recycle(); +} + + + + + + + + Sending Messages + + + + Sending a message is the same for both the server and client, you simply call M:Connection.SendBytes on your connection. + Add the following line after the call to Connect + +connection.SendBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + + + + + + + You should have something like this. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Hazel; +using Hazel.Tcp; + +namespace HazelExample +{ + class ClientExample + { + static Connection connection; + + public static void Main(string[] args) + { + NetworkEndPoint endPoint = new NetworkEndPoint("127.0.0.1", 4296); + + connection = new TcpConnection(endPoint); + + connection.DataReceived += DataReceived; + + Console.WriteLine("Connecting!"); + + connection.Connect(); + + connection.SendBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + + Console.WriteLine("Press any key to continue..."); + + Console.ReadKey(); + + connection.Close(); + } + + private static void DataReceived(object sender, DataEventArgs args) + { + Console.WriteLine("Received (" + string.Join<byte>(", ", args.Bytes) + ") from " + connection.EndPoint.ToString()); + + args.Recycle(); + } + } +} + + +
+
+
\ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Technical Details/Protocols/Protocols.aml b/Hazel/Documentation/Documentation/Technical Details/Protocols/Protocols.aml new file mode 100644 index 0000000..46e03af --- /dev/null +++ b/Hazel/Documentation/Documentation/Technical Details/Protocols/Protocols.aml @@ -0,0 +1,14 @@ + + + + + + + In order to provide the guarantees that it does, Hazel augments each + transport protocol with its own meta data (or header data) that is + hidden from users. + + + + + \ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Technical Details/Protocols/TCP.aml b/Hazel/Documentation/Documentation/Technical Details/Protocols/TCP.aml new file mode 100644 index 0000000..a6259c4 --- /dev/null +++ b/Hazel/Documentation/Documentation/Technical Details/Protocols/TCP.aml @@ -0,0 +1,33 @@ + + + + + + + Hazel's TCP protocol is fairly simple as TCP already provides connection + based communication and thus only needs a message system implementing. + + + +
+ Header Data + + + The TCP protocol is fairly simple interms of header. 4 bytes are + added to mark the number of bytes in each message. + + + + Length (MSB) + Length + Length + Length (LSB) + Data... + +
+ All header data in Hazel is encoded in big endian format. +
+
+ +
+
\ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Technical Details/Protocols/UDP-RUDP.aml b/Hazel/Documentation/Documentation/Technical Details/Protocols/UDP-RUDP.aml new file mode 100644 index 0000000..31dfb62 --- /dev/null +++ b/Hazel/Documentation/Documentation/Technical Details/Protocols/UDP-RUDP.aml @@ -0,0 +1,146 @@ + + + + + + + UDP provides message based communication however it is not connection + oriented nor does it have any built in functionality for reliable or + fragmented delivery of messages hence the implementation to cover this + if fairly complicated. + + + +
+ Header Data + + + The UDP protocol has multiple layers of header data depending on the + send options that are requested with the message. + + + + Unreliable + Type + Data... +   +   + + + Reliable + Type + ID (MSB) + ID (LSB) + Data... + +
+ All header data in Hazel is encoded in big endian format. + + In both of these, Type is an identifier + that holds the SendOption or another value indicating a control + packet of data. The most significant 4 bits of + Type are reserved for future use and the + least significant bits specify the send option flags for the message. + + + + + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 + + + + Reserved + Reserved + Reserved + Reserved + Control + Reserved + Fragmented + Reliable + +
+ + Reliable and + Fragmented are both flags for their + respective send option, Control, if set, + specifies that the 3 least significant bits refer to a control code: + + + + 0 + Hello + + + 1 + Disconnect + + + 2 + Acknowledgment + +
+
+
+ +
+ Reliable Delivery + + + To implement reliable delivery 2 ID bytes are sent which identify the + packet. When the receiver receives the data is replys with an + acknowledgement packet as follows: + + + + Acknowledgement + Type + ID (MSB) + ID (LSB) + +
+ + Where type is specifying an acknowledgement packet as laid out above. + + + If the sending client does not receive an acknowledgement after a + specific amount of time elapses from the pakcet being sent then it + resends that packet and thetime before the next resend of that packet + is doubled. When the sending client receives the acknowledgement is + should not resend the data again. + + + The receiving client must also ensure that it does not present the + same packet to the user twice but must acknowledge any packets it + receives, even if it has already received that packet, in case an + acknowledgement is lost. + + + After a specific number of resends without acknowledgement a sending + client may mark assume that communication has been interrupted and + thus mark the connection as disconnected. + +
+
+ +
+ Keepalive packets + + + Keepalive packets should be sent after a specific time has elapsed + since the last packet (either keepalive, acknowledgement or user sent) + was transmitted and should be sent using reliable delivery so that an + acknowledgement can be received to indicate communication has not been + lost. As packets that are not acknowledged should cause a + disconnection no additional logic is required for keepalives. + + +
+
+
\ No newline at end of file diff --git a/Hazel/Documentation/Documentation/Technical Details/Technical Details.aml b/Hazel/Documentation/Documentation/Technical Details/Technical Details.aml new file mode 100644 index 0000000..9b4729e --- /dev/null +++ b/Hazel/Documentation/Documentation/Technical Details/Technical Details.aml @@ -0,0 +1,14 @@ + + + + + + + Underneath Hazel there are a lot of technicalities, this section will + help outline those details so you understand Hazel's implementations + better. + + + + + \ No newline at end of file diff --git a/Hazel/Documentation/Hazel.shfbproj b/Hazel/Documentation/Hazel.shfbproj new file mode 100644 index 0000000..f46d602 --- /dev/null +++ b/Hazel/Documentation/Hazel.shfbproj @@ -0,0 +1,98 @@ + + + + + Debug + AnyCPU + 2.0 + {359995e0-bef3-42dd-800f-20368ff5fabb} + 2015.6.5.0 + + Documentation + Documentation + Documentation + + .NET Framework 4.5 + .\Help\ + Documentation + en-US + 100 + OnlyWarningsAndErrors + Website + False + True + False + True + + + + 1.0.0.0 + 2 + False + C# + Blank + False + VS2013 + False + Guid + Hazel Networking + AboveNamespaces + + The Hazel namespace contains various classes used across the different communication methods implemented. +Namespace for classes relating to communication via TCP. +Namespace for classes relating to communication via TCP. + + + + + + + + + + + + + + + + + + + + + + + + + + + OnBuildSuccess + + + + + + + + + + + + + + + + + + + + + Hazel + {02CFBD30-D77D-400F-94B2-700F60EFDD7F} + + + \ No newline at end of file diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index ce4bf63..1bf7a70 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -70,18 +70,18 @@ - + - - - - + + + + Code - - - - + + + + diff --git a/Hazel/Hazel.shfbproj b/Hazel/Hazel.shfbproj deleted file mode 100644 index cd9f323..0000000 --- a/Hazel/Hazel.shfbproj +++ /dev/null @@ -1,77 +0,0 @@ - - - - - Debug - AnyCPU - 2.0 - {359995e0-bef3-42dd-800f-20368ff5fabb} - 2015.6.5.0 - - Documentation - Documentation - Documentation - - .NET Framework 4.5 - .\Help\ - Documentation - en-US - 100 - OnlyWarningsAndErrors - Website - False - True - False - True - - - - 1.0.0.0 - 2 - False - C# - Blank - False - VS2013 - False - Guid - Hazel Networking - AboveNamespaces - - - - - - - - - - - - - - - - - - - - - - - - - - - OnBuildSuccess - - - - Hazel - {02CFBD30-D77D-400F-94B2-700F60EFDD7F} - - - \ No newline at end of file diff --git a/Hazel/NetworkEndPoint.cs b/Hazel/NetworkEndPoint.cs index aa114a3..7e71171 100644 --- a/Hazel/NetworkEndPoint.cs +++ b/Hazel/NetworkEndPoint.cs @@ -30,7 +30,8 @@ namespace Hazel /// /// Creates a NetworkEndPoint from a given EndPoint. /// - /// The end point to wrap./param> + /// The end point to wrap. + /// The IP mode to use. public NetworkEndPoint(EndPoint endPoint, IPMode mode = IPMode.IPv4AndIPv6) { this.EndPoint = endPoint; @@ -42,6 +43,7 @@ namespace Hazel /// /// The IP address of the server. /// The port the server is listening on. + /// The IP mode to use. /// /// When using this constructor will contain an . /// @@ -56,13 +58,20 @@ namespace Hazel /// /// A valid IP address of the server. /// The port the server is listening on. + /// The IP mode to use. /// /// When using this constructor will contain an . /// public NetworkEndPoint(string IP, int port, IPMode mode = IPMode.IPv4AndIPv6) : this(IPAddress.Parse(IP), port) { + + } + /// + public override string ToString() + { + return EndPoint.ToString(); } } } diff --git a/Hazel/SendOption.cs b/Hazel/SendOption.cs index c45c08e..2629711 100644 --- a/Hazel/SendOption.cs +++ b/Hazel/SendOption.cs @@ -31,7 +31,7 @@ namespace Hazel /// typically requires more processing, more memory (as packets need to be stored in case they need resending), /// a larger number of protocol bytes and can be slower than unreliable delivery. /// - Reliable = 16, + Reliable = 1, /// /// Requests data be sent so that large messages are fragmented into smaller chunks of @@ -42,7 +42,7 @@ namespace Hazel /// that do not support the transmission of large messages. Without specifying reliable delivery there is no /// guarentee that the message will arrive but any incomplete messages will be simply be discarded. /// - Fragmented = 32, + Fragmented = 2, /// /// Requests data be sent so that large messages are fragmented into smaller chunks of @@ -54,6 +54,6 @@ namespace Hazel /// guaranteed to arrive and to arrive only once but the sending process may require more memory, processing, /// a larger number protocol bytes and may be slower than sending unreliably. /// - FragmentedReliable = 48 + FragmentedReliable = 3 } } diff --git a/Hazel/SendOptionInternal.cs b/Hazel/SendOptionInternal.cs index 212467d..e39e7e5 100644 --- a/Hazel/SendOptionInternal.cs +++ b/Hazel/SendOptionInternal.cs @@ -14,16 +14,16 @@ namespace Hazel /// /// Hello message for initiating communication. /// - Hello = 128, + Hello = 8, /// /// Message for discontinuing communication. /// - Disconnect = 129, + Disconnect = 9, /// /// Message acknowledging the receipt of a message. /// - Acknowledgement = 130 + Acknowledgement = 10 } } diff --git a/Hazel/StateObject.cs b/Hazel/StateObject.cs deleted file mode 100644 index b025b5a..0000000 --- a/Hazel/StateObject.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Hazel -{ - /// - /// Represents the state of the current receive operation for TCP connections. - /// - struct StateObject - { - /// - /// The buffer we're receiving. - /// - internal byte[] buffer; - - /// - /// The total number of bytes received so far. - /// - internal int totalBytesReceived; - - /// - /// The callback to invoke once the buffer has been filled. - /// - internal Action callback; - - /// - /// Creates a StateObject with the specified length. - /// - /// The number of bytes expected to be received. - internal StateObject(int length, Action callback) - { - this.buffer = new byte[length]; - this.totalBytesReceived = 0; - this.callback = callback; - } - } -} diff --git a/Hazel/Tcp/StateObject.cs b/Hazel/Tcp/StateObject.cs new file mode 100644 index 0000000..c5878f9 --- /dev/null +++ b/Hazel/Tcp/StateObject.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel.Tcp +{ + /// + /// Represents the state of the current receive operation for TCP connections. + /// + struct StateObject + { + /// + /// The buffer we're receiving. + /// + internal byte[] buffer; + + /// + /// The total number of bytes received so far. + /// + internal int totalBytesReceived; + + /// + /// The callback to invoke once the buffer has been filled. + /// + internal Action callback; + + /// + /// Creates a StateObject with the specified length. + /// + /// The number of bytes expected to be received. + /// The callback to invoke once data has been received. + internal StateObject(int length, Action callback) + { + this.buffer = new byte[length]; + this.totalBytesReceived = 0; + this.callback = callback; + } + } +} diff --git a/Hazel/Tcp/TcpConnection.cs b/Hazel/Tcp/TcpConnection.cs new file mode 100644 index 0000000..62e32cb --- /dev/null +++ b/Hazel/Tcp/TcpConnection.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Hazel.Tcp +{ + /// + /// Represents a connection that uses the TCP protocol. + /// + /// + public sealed class TcpConnection : NetworkConnection + { + /// + /// The socket we're managing. + /// + Socket socket; + + /// + /// Lock for the socket. + /// + Object socketLock = new Object(); + + /// + /// Creates a TcpConnection from a given TCP Socket. + /// + /// The TCP socket to wrap. + internal TcpConnection(Socket socket) + { + //Check it's a TCP socket + if (socket.ProtocolType != System.Net.Sockets.ProtocolType.Tcp) + throw new ArgumentException("A TcpConnection requires a TCP socket."); + + lock (this.socketLock) + { + this.EndPoint = new NetworkEndPoint(socket.RemoteEndPoint); + this.RemoteEndPoint = socket.RemoteEndPoint; + + this.socket = socket; + this.socket.NoDelay = true; + + State = ConnectionState.Connected; + } + } + + /// + /// Creates a new TCP connection. + /// + /// A to connect to. + public TcpConnection(NetworkEndPoint remoteEndPoint) + { + lock (socketLock) + { + if (State != ConnectionState.NotConnected) + throw new InvalidOperationException("Cannot connect as the Connection is already connected."); + + this.EndPoint = remoteEndPoint; + this.RemoteEndPoint = remoteEndPoint.EndPoint; + + //Create a socket + if (remoteEndPoint.IPMode == IPMode.IPv4) + socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + else + { + if (!Socket.OSSupportsIPv6) + throw new HazelException("IPV6 not supported!"); + + socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + } + + //Set parameters of socket + if (remoteEndPoint.IPMode == IPMode.IPv4AndIPv6) + socket.DualMode = true; + + socket.NoDelay = true; + } + } + + /// + /// Internal call to start listening once this socket has been constructed and is ready. + /// + internal void StartListening() + { + //Start receiving data + try + { + StartWaitingForHeader(); + } + catch (SocketException e) + { + throw new HazelException("A Socket exception occured while initiating a receive operation.", e); + } + } + + /// + public override void Connect() + { + lock(socketLock) + { + //Connect + State = ConnectionState.Connecting; + + try + { + socket.Connect(RemoteEndPoint); + } + catch (SocketException e) + { + throw new HazelException("Could not connect as a socket exception occured.", e); + } + + //Start receiving data + StartListening(); + + //Set connected + State = ConnectionState.Connected; + } + } + + /// + /// + /// + /// + /// The sendOption parameter is ignored by the TcpConnection as TCP only supports FragmentedReliable + /// communication, specifying anything else will have no effect. + /// + /// + public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption.FragmentedReliable) + { + //Get bytes for length + byte[] fullBytes = AppendLengthHeader(bytes); + + //Write the bytes to the socket + lock (socketLock) + { + if (State != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + + try + { + socket.BeginSend(fullBytes, 0, fullBytes.Length, SocketFlags.None, null, null); + } + catch (SocketException e) + { + HazelException he = new HazelException("Could not send data as a SocketException occured.", e); + HandleDisconnect(he); + throw he; + } + } + + Statistics.LogSend(bytes.Length, fullBytes.Length); + } + + /// + /// Called when a 4 byte header has been received. + /// + /// The 4 header bytes read. + void HeaderReadCallback(byte[] bytes) + { + //Get length + int length = GetLengthFromBytes(bytes); + + //Begin receiving the body + try + { + StartWaitingForBytes(length, BodyReadCallback); + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); + } + } + + /// + /// Callback for when a body has been read. + /// + /// The data bytes received by the connection. + void BodyReadCallback(byte[] bytes) + { + //Begin receiving from the start + StartWaitingForHeader(); + + Statistics.LogReceive(bytes.Length, bytes.Length + 4); + + //Fire DataReceived event + InvokeDataReceived(bytes, SendOption.FragmentedReliable); + } + + /// + /// Starts this connections waiting for the header. + /// + void StartWaitingForHeader() + { + StartWaitingForBytes(4, HeaderReadCallback); + } + + /// + /// Waits for the specified amount of bytes to be received. + /// + /// The number of bytes to receive. + /// The callback + void StartWaitingForBytes(int length, Action callback) + { + StateObject state = new StateObject(length, callback); + + StartWaitingForChunk(state); + } + + /// + /// Waits for the next chunk of data from this socket. + /// + /// The StateObject for the receive operation. + void StartWaitingForChunk(StateObject state) + { + lock (socketLock) + { + //Double check we've not disconnected then begin receiving + if (State == ConnectionState.Connected || State == ConnectionState.Connecting) + socket.BeginReceive(state.buffer, state.totalBytesReceived, state.buffer.Length, SocketFlags.None, ChunkReadCallback, state); + else + HandleDisconnect(); + } + } + + /// + /// Called when a chunk has been read. + /// + /// + void ChunkReadCallback(IAsyncResult result) + { + int bytesReceived; + + //End the receive operation + try + { + lock (socketLock) + bytesReceived = socket.EndReceive(result); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); + return; + } + + StateObject state = (StateObject)result.AsyncState; + + state.totalBytesReceived += bytesReceived; + + //Exit if receive nothing + if (bytesReceived == 0) + { + HandleDisconnect(); + return; + } + + //If we need to receive more then wait for more, else process it. + if (state.totalBytesReceived < state.buffer.Length) + { + try + { + StartWaitingForChunk(state); + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); + return; + } + } + else + state.callback.Invoke(state.buffer); + } + + /// + /// Called when the socket has been disconnected at the remote host. + /// + /// The exception if one was the cause. + void HandleDisconnect(HazelException e = null) + { + bool invoke = false; + + lock (socketLock) + { + //Only invoke the disconnected event if we're not already disconnecting + if (State == ConnectionState.Connected) + { + State = ConnectionState.Disconnecting; + invoke = true; + } + } + + //Invoke event outide lock if need be + if (invoke) + { + InvokeDisconnected(e); + + Dispose(); + } + } + + /// + /// Appends the length header to the bytes. + /// + /// The source bytes. + /// The new bytes. + static byte[] AppendLengthHeader(byte[] bytes) + { + byte[] fullBytes = new byte[bytes.Length + 4]; + + //Append length + fullBytes[0] = (byte)(((uint)bytes.Length >> 24) & 0xFF); + fullBytes[1] = (byte)(((uint)bytes.Length >> 16) & 0xFF); + fullBytes[2] = (byte)(((uint)bytes.Length >> 8) & 0xFF); + fullBytes[3] = (byte)(uint)bytes.Length; + + //Add rest of bytes + Buffer.BlockCopy(bytes, 0, fullBytes, 4, bytes.Length); + + return fullBytes; + } + + /// + /// Returns the length from a length header. + /// + /// The bytes received. + /// The number of bytes. + static int GetLengthFromBytes(byte[] bytes) + { + if (bytes.Length < 4) + throw new IndexOutOfRangeException("Not enough bytes passed to calculate length."); + + return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (socketLock) + { + State = ConnectionState.NotConnected; + + if (socket.Connected) + socket.Shutdown(SocketShutdown.Send); + socket.Dispose(); + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/Tcp/TcpConnectionListener.cs b/Hazel/Tcp/TcpConnectionListener.cs new file mode 100644 index 0000000..055febe --- /dev/null +++ b/Hazel/Tcp/TcpConnectionListener.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel.Tcp +{ + /// + /// Listens for new TCP connections and creates TCPConnections for them. + /// + /// + public sealed class TcpConnectionListener : NetworkConnectionListener + { + /// + /// The socket listening for connections. + /// + Socket listener; + + /// + /// Creates a new TcpConnectionListener for the given , port and . + /// + /// The IPAddress to listen on. + /// The port to listen on. + /// The to listen with. + public TcpConnectionListener(IPAddress IPAddress, int port, IPMode mode = IPMode.IPv4AndIPv6) + { + this.IPAddress = IPAddress; + this.Port = port; + + if (mode == IPMode.IPv4) + this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + else + { + if (!Socket.OSSupportsIPv6) + throw new HazelException("IPV6 not supported!"); + + this.listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + } + + if (mode == IPMode.IPv4AndIPv6) + this.listener.DualMode = true; + } + + /// + public override void Start() + { + try + { + lock (listener) + { + listener.Bind(new IPEndPoint(IPAddress, Port)); + listener.Listen(1000); + + listener.BeginAccept(AcceptConnection, null); + } + } + catch (SocketException e) + { + throw new HazelException("Could not start listening as a SocketException occured", e); + } + } + + /// + /// Called when a new connection has been accepted by the listener. + /// + /// The asyncronous operation's result. + void AcceptConnection(IAsyncResult result) + { + lock (listener) + { + //Accept Tcp socket + Socket tcpSocket; + try + { + tcpSocket = listener.EndAccept(result); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + + //Start listening for the next connection + listener.BeginAccept(new AsyncCallback(AcceptConnection), null); + + //Sort the event out + TcpConnection tcpConnection = new TcpConnection(tcpSocket); + + //Invoke + InvokeNewConnection(tcpConnection); + + tcpConnection.StartListening(); + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (listener) + listener.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/TcpConnection.cs b/Hazel/TcpConnection.cs deleted file mode 100644 index db122f4..0000000 --- a/Hazel/TcpConnection.cs +++ /dev/null @@ -1,364 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; - -namespace Hazel -{ - /// - /// Represents a connection that uses the TCP protocol. - /// - /// - public sealed class TcpConnection : NetworkConnection - { - /// - /// The socket we're managing. - /// - Socket socket; - - /// - /// Lock for the socket. - /// - Object socketLock = new Object(); - - /// - /// Creates a TcpConnection from a given TCP Socket. - /// - /// The TCP socket to wrap. - internal TcpConnection(Socket socket) - { - //Check it's a TCP socket - if (socket.ProtocolType != System.Net.Sockets.ProtocolType.Tcp) - throw new ArgumentException("A TcpConnection requires a TCP socket."); - - lock (this.socketLock) - { - this.EndPoint = new NetworkEndPoint(socket.RemoteEndPoint); - this.RemoteEndPoint = socket.RemoteEndPoint; - - this.socket = socket; - this.socket.NoDelay = true; - - State = ConnectionState.Connected; - } - } - - /// - /// Creates a new TCP connection. - /// - public TcpConnection() - { - - } - - /// - /// Internal call to start listening once this socket has been constructed and is ready. - /// - internal void StartListening() - { - //Start receiving data - try - { - StartWaitingForHeader(); - } - catch (SocketException e) - { - throw new HazelException("A Socket exception occured while initiating a receive operation.", e); - } - } - - /// - /// A to connect to. - public override void Connect(ConnectionEndPoint remoteEndPoint) - { - NetworkEndPoint nep = remoteEndPoint as NetworkEndPoint; - if (nep == null) - { - throw new ArgumentException("The remote end point of a TCP connection must be a NetworkEndPoint."); - } - - lock (socketLock) - { - if (State != ConnectionState.NotConnected) - throw new InvalidOperationException("Cannot connect as the Connection is already connected."); - - this.EndPoint = remoteEndPoint; - this.RemoteEndPoint = nep.EndPoint; - - //Create a socket - if (nep.IPMode == IPMode.IPv4) - socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - else - { - if (!Socket.OSSupportsIPv6) - throw new HazelException("IPV6 not supported!"); - - socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - } - - //Set parameters of socket - if (nep.IPMode == IPMode.IPv4AndIPv6) - socket.DualMode = true; - - socket.NoDelay = true; - - //Connect - State = ConnectionState.Connecting; - - try - { - socket.Connect(nep.EndPoint); - } - catch (SocketException e) - { - throw new HazelException("Could not connect as a socket exception occured.", e); - } - - //Start receiving data - StartListening(); - - //Set connected - State = ConnectionState.Connected; - } - } - - /// - /// - /// - /// - /// The sendOption parameter is ignored by the TcpConnection as TCP only supports FragmentedReliable - /// communication, specifying anything else will have no effect. - /// - /// - public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption.FragmentedReliable) - { - //Get bytes for length - byte[] fullBytes = AppendLengthHeader(bytes); - - //Write the bytes to the socket - lock (socketLock) - { - if (State != ConnectionState.Connected) - throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); - - try - { - socket.BeginSend(fullBytes, 0, fullBytes.Length, SocketFlags.None, null, null); - } - catch (SocketException e) - { - HazelException he = new HazelException("Could not send data as a SocketException occured.", e); - HandleDisconnect(he); - throw he; - } - } - - Statistics.LogSend(bytes.Length, fullBytes.Length); - } - - /// - /// Called when a 4 byte header has been received. - /// - /// The 4 header bytes read. - void HeaderReadCallback(byte[] bytes) - { - //Get length - int length = GetLengthFromBytes(bytes); - - //Begin receiving the body - try - { - StartWaitingForBytes(length, BodyReadCallback); - } - catch (SocketException e) - { - HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); - } - } - - /// - /// Callback for when a body has been read. - /// - /// The data bytes received by the connection. - void BodyReadCallback(byte[] bytes) - { - //Begin receiving from the start - StartWaitingForHeader(); - - Statistics.LogReceive(bytes.Length, bytes.Length + 4); - - //Fire DataReceived event - InvokeDataReceived(bytes, SendOption.FragmentedReliable); - } - - /// - /// Starts this connections waiting for the header. - /// - void StartWaitingForHeader() - { - StartWaitingForBytes(4, HeaderReadCallback); - } - - /// - /// Waits for the specified amount of bytes to be received. - /// - /// The number of bytes to receive. - /// The callback - void StartWaitingForBytes(int length, Action callback) - { - StateObject state = new StateObject(length, callback); - - StartWaitingForChunk(state); - } - - /// - /// Waits for the next chunk of data from this socket. - /// - /// The StateObject for the receive operation. - void StartWaitingForChunk(StateObject state) - { - lock (socketLock) - { - //Double check we've not disconnected then begin receiving - if (State == ConnectionState.Connected || State == ConnectionState.Connecting) - socket.BeginReceive(state.buffer, state.totalBytesReceived, state.buffer.Length, SocketFlags.None, ChunkReadCallback, state); - else - HandleDisconnect(); - } - } - - /// - /// Called when a chunk has been read. - /// - /// - void ChunkReadCallback(IAsyncResult result) - { - int bytesReceived; - - //End the receive operation - try - { - lock (socketLock) - bytesReceived = socket.EndReceive(result); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there. - return; - } - catch (SocketException e) - { - HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); - return; - } - - StateObject state = (StateObject)result.AsyncState; - - state.totalBytesReceived += bytesReceived; - - //Exit if receive nothing - if (bytesReceived == 0) - { - HandleDisconnect(); - return; - } - - //If we need to receive more then wait for more, else process it. - if (state.totalBytesReceived < state.buffer.Length) - { - try - { - StartWaitingForChunk(state); - } - catch (SocketException e) - { - HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); - return; - } - } - else - state.callback.Invoke(state.buffer); - } - - /// - /// Called when the socket has been disconnected at the remote host. - /// - /// The exception if one was the cause. - void HandleDisconnect(HazelException e = null) - { - bool invoke = false; - - lock (socketLock) - { - //Only invoke the disconnected event if we're not already disconnecting - if (State == ConnectionState.Connected) - { - State = ConnectionState.Disconnecting; - invoke = true; - } - } - - //Invoke event outide lock if need be - if (invoke) - { - InvokeDisconnected(e); - - Dispose(); - } - } - - /// - /// Appends the length header to the bytes. - /// - /// The source bytes. - /// The new bytes. - static byte[] AppendLengthHeader(byte[] bytes) - { - byte[] fullBytes = new byte[bytes.Length + 4]; - - //Append length - fullBytes[0] = (byte)(((uint)bytes.Length >> 24) & 0xFF); - fullBytes[1] = (byte)(((uint)bytes.Length >> 16) & 0xFF); - fullBytes[2] = (byte)(((uint)bytes.Length >> 8) & 0xFF); - fullBytes[3] = (byte)(uint)bytes.Length; - - //Add rest of bytes - Buffer.BlockCopy(bytes, 0, fullBytes, 4, bytes.Length); - - return fullBytes; - } - - /// - /// Returns the length from a length header. - /// - /// The bytes received. - /// The number of bytes. - static int GetLengthFromBytes(byte[] bytes) - { - if (bytes.Length < 4) - throw new IndexOutOfRangeException("Not enough bytes passed to calculate length."); - - return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - lock (socketLock) - { - State = ConnectionState.NotConnected; - - if (socket.Connected) - socket.Shutdown(SocketShutdown.Send); - socket.Dispose(); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Hazel/TcpConnectionListener.cs b/Hazel/TcpConnectionListener.cs deleted file mode 100644 index 2eaa8b4..0000000 --- a/Hazel/TcpConnectionListener.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; - -//TODO replace copyright notices with MIT licenses - -namespace Hazel -{ - /// - /// Listens for new TCP connections and creates TCPConnections for them. - /// - /// - public sealed class TcpConnectionListener : NetworkConnectionListener - { - /// - /// The socket listening for connections. - /// - Socket listener; - - /// - /// Creates a new TcpConnectionListener for the given , port and . - /// - /// The IPAddress to listen on. - /// The port to listen on. - /// The to listen with. - public TcpConnectionListener(IPAddress IPAddress, int port, IPMode mode = IPMode.IPv4AndIPv6) - { - this.IPAddress = IPAddress; - this.Port = port; - - if (mode == IPMode.IPv4) - this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - else - { - if (!Socket.OSSupportsIPv6) - throw new HazelException("IPV6 not supported!"); - - this.listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); - } - - if (mode == IPMode.IPv4AndIPv6) - this.listener.DualMode = true; - } - - /// - public override void Start() - { - try - { - lock (listener) - { - listener.Bind(new IPEndPoint(IPAddress, Port)); - listener.Listen(1000); - - listener.BeginAccept(AcceptConnection, null); - } - } - catch (SocketException e) - { - throw new HazelException("Could not start listening as a SocketException occured", e); - } - } - - /// - /// Called when a new connection has been accepted by the listener. - /// - /// The asyncronous operation's result. - void AcceptConnection(IAsyncResult result) - { - lock (listener) - { - //Accept Tcp socket - Socket tcpSocket; - try - { - tcpSocket = listener.EndAccept(result); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there. - return; - } - - //Start listening for the next connection - listener.BeginAccept(new AsyncCallback(AcceptConnection), null); - - //Sort the event out - TcpConnection tcpConnection = new TcpConnection(tcpSocket); - - //Invoke - InvokeNewConnection(tcpConnection); - - tcpConnection.StartListening(); - } - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - lock (listener) - listener.Dispose(); - } - - base.Dispose(disposing); - } - } -} diff --git a/Hazel/Udp/UdpClientConnection.cs b/Hazel/Udp/UdpClientConnection.cs new file mode 100644 index 0000000..c0df18b --- /dev/null +++ b/Hazel/Udp/UdpClientConnection.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Hazel.Udp +{ + /// + /// Represents a client's connection to a server that uses the UDP protocol. + /// + /// + public sealed class UdpClientConnection : UdpConnection + { + /// + /// The socket we're connected via. + /// + Socket socket; + + /// + /// The lock for the socket. + /// + Object socketLock = new Object(); + + /// + /// The buffer to store incomming data in. + /// + byte[] dataBuffer = new byte[ushort.MaxValue]; + + /// + /// Creates a new UdpClientConnection. + /// + /// A to connect to. + public UdpClientConnection(NetworkEndPoint remoteEndPoint) + : base() + { + lock (socketLock) + { + this.EndPoint = remoteEndPoint; + this.RemoteEndPoint = remoteEndPoint.EndPoint; + + if (remoteEndPoint.IPMode == IPMode.IPv4) + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + else + { + socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + socket.DualMode = true; + } + } + } + + /// + protected override void WriteBytesToConnection(byte[] bytes) + { + //Pack + SocketAsyncEventArgs args = new SocketAsyncEventArgs(); + args.SetBuffer(bytes, 0, bytes.Length); + args.RemoteEndPoint = RemoteEndPoint; + + lock (socketLock) + { + if (State != ConnectionState.Connected && State != ConnectionState.Connecting) + throw new InvalidOperationException("Could not send data as this Connection is not connected and is not connecting. Did you disconnect?"); + + try + { + socket.SendToAsync(args); + } + catch (ObjectDisposedException) + { + //User probably called Disconnect in between this method starting and here so report the issue + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + } + catch (SocketException e) + { + HazelException he = new HazelException("Could not send data as a SocketException occured.", e); + HandleDisconnect(he); + throw he; + } + } + } + + /// + public override void Connect() + { + lock(socketLock) + { + if (State != ConnectionState.NotConnected) + throw new InvalidOperationException("Cannot connect as the Connection is already connected."); + + State = ConnectionState.Connecting; + + //Begin listening + try + { + socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + } + catch (SocketException e) + { + throw new HazelException("A socket exception occured while binding to the port.", e); + } + + try + { + StartListeningForData(); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there but make sure we're in NotConnected state. + //If we end up here I'm really lost... + State = ConnectionState.NotConnected; + return; + } + catch (SocketException e) + { + throw new HazelException("A Socket exception occured while initiating a receive operation.", e); + } + } + + //Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present) + //When acknowledged set the state to connected + SendHello(() => { lock (socketLock) State = ConnectionState.Connected; }); + + //Wait till hello packet is acknowledged and the state is set to Connected + WaitOnConnect(); + } + + /// + /// Instructs the listener to begin listening. + /// + void StartListeningForData() + { + lock (socketLock) + socket.BeginReceive(dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ReadCallback, dataBuffer); + } + + /// + /// Called when data has been received by the socket. + /// + /// The asyncronous operation's result. + void ReadCallback(IAsyncResult result) + { + int bytesReceived; + + //End the receive operation + try + { + lock (socketLock) + bytesReceived = socket.EndReceive(result); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A socket exception occured while reading data.", e)); + return; + } + + //Exit if no bytes read, we've failed. + if (bytesReceived == 0) + { + HandleDisconnect(); + return; + } + + //Decode the data received + byte[] buffer = HandleReceive(dataBuffer, bytesReceived); + SendOption sendOption = (SendOption)dataBuffer[0]; + + //TODO may get better performance with Handle receive after and block copy call added + + //Begin receiving again + try + { + StartListeningForData(); + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + + if (buffer != null) + InvokeDataReceived(buffer, sendOption); + } + + /// + protected override void HandleDisconnect(HazelException e = null) + { + bool invoke = false; + + lock (socketLock) + { + //Only invoke the disconnected event if we're not already disconnecting + if (State == ConnectionState.Connected) + { + State = ConnectionState.Disconnecting; + invoke = true; + } + } + + //Invoke event outide lock if need be + if (invoke) + { + InvokeDisconnected(e); + + Dispose(); + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + //Send disconnect message if we're not already disconnecting + if (State == ConnectionState.Connected) + SendDisconnect(); + + //Dispose of the socket + lock (socketLock) + { + State = ConnectionState.NotConnected; + + socket.Dispose(); + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/Udp/UdpConnection.KeepAlive.cs b/Hazel/Udp/UdpConnection.KeepAlive.cs new file mode 100644 index 0000000..f2864f7 --- /dev/null +++ b/Hazel/Udp/UdpConnection.KeepAlive.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Hazel.Udp +{ + partial class UdpConnection + { + /// + /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds. + /// + /// + /// + /// Keepalive packets serve to close connections when an endpoint abruptly disconnects and to ensure than any + /// NAT devices do not close their translation for our argument. By ensuring there is regular contact the + /// connection can detect and prevent these issues. + /// + /// + /// The default value is 10 seconds, set to System.Threading.Timeout.Infinite to disable keepalive packets. + /// + /// + public int KeepAliveInterval + { + get + { + return keepAliveInterval; + } + + set + { + keepAliveInterval = value; + + //Update timer + ResetKeepAliveTimer(); + } + } + int keepAliveInterval = 10000; + + /// + /// The timer creating keepalive pulses. + /// + Timer keepAliveTimer; + + /// + /// Lock for keep alive timer. + /// + Object keepAliveTimerLock = new Object(); + + /// + /// Has the keep alive timer been disposed already? + /// + bool keepAliveTimerDisposed; + + /// + /// Starts the keepalive timer. + /// + void InitializeKeepAliveTimer() + { + lock (keepAliveTimerLock) + { + keepAliveTimer = new Timer( + (o) => + { + Trace.WriteLine("Keepalive packet sent."); + SendHello(null); + }, + null, + keepAliveInterval, + keepAliveInterval + ); + } + } + + /// + /// Resets the keepalive timer to zero. + /// + void ResetKeepAliveTimer() + { + lock (keepAliveTimerLock) + keepAliveTimer.Change(keepAliveInterval, keepAliveInterval); + } + + /// + /// Disposes of the keep alive timer. + /// + void DisposeKeepAliveTimer() + { + lock(keepAliveTimerLock) + { + if (!keepAliveTimerDisposed) + keepAliveTimer.Dispose(); + keepAliveTimerDisposed = true; + } + } + } +} diff --git a/Hazel/Udp/UdpConnection.Reliable.cs b/Hazel/Udp/UdpConnection.Reliable.cs new file mode 100644 index 0000000..2c40fc1 --- /dev/null +++ b/Hazel/Udp/UdpConnection.Reliable.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Hazel.Udp +{ + partial class UdpConnection + { + /// + /// The starting timeout, in miliseconds, at which data will be resent. + /// + /// + /// For reliable delivery data is resent at specified intervals unless an acknowledgement is received from the + /// receiving device. The ResendTimeout specifies the interval between the packets being resent, each time a packet + /// is resent the interval is doubled for that packet until the number of resends exceeds the + /// value. + /// + public int ResendTimeout { get { return resendTimeout; } set { resendTimeout = value; } } + private volatile int resendTimeout = 200; //TODO this based of average ping? + + /// + /// Holds the last ID allocated. + /// + volatile ushort lastIDAllocated; + + /// + /// The packets of data that have been transmitted reliably and not acknowledged. + /// + Dictionary reliableDataPacketsSent = new Dictionary(); + + /// + /// The last packets that were received. + /// + HashSet reliableDataPacketsMissing = new HashSet(); + + /// + /// The packet id that was received last. + /// + volatile ushort reliableReceiveLast = 0; + + /// + /// Has the connection received anything yet + /// + volatile bool hasReceivedSomething = false; + + /// + /// The maximum times a message should be resent before marking the endpoint as disconnected. + /// + /// + /// Reliable packets will be resent at an interval defined in for the number of times + /// specified here. Once a packet has been retransmitted this number of times and has not been acknowledged the + /// connection will be marked as disconnected and the Disconnected event + /// will be invoked. + /// + public int ResendsBeforeDisconnect { get { return resendsBeforeDisconnect; } set { resendsBeforeDisconnect = value; } } + private volatile int resendsBeforeDisconnect = 3; + + /// + /// Class to hold packet data + /// + class Packet : IRecyclable, IDisposable + { + /// + /// Object pool for this event. + /// + static readonly ObjectPool objectPool = new ObjectPool(() => new Packet()); + + /// + /// Returns an instance of this object from the pool. + /// + /// + internal static Packet GetObject() + { + return objectPool.GetObject(); + } + + public byte[] Data; + public Timer Timer; + public volatile int LastTimeout; + public Action AckCallback; + public volatile bool Acknowledged; + public volatile int Retransmissions; + + Packet() + { + + } + + internal void Set(byte[] data, Action resendAction, int timeout, Action ackCallback) + { + Data = data; + + Timer = new Timer( + (object obj) => resendAction(this), + null, + timeout, + Timeout.Infinite + ); + + LastTimeout = timeout; + AckCallback = ackCallback; + Acknowledged = false; + Retransmissions = 0; + } + + /// + /// Returns this object back to the object pool from whence it came. + /// + public void Recycle() + { + lock (Timer) + Timer.Dispose(); + + objectPool.PutObject(this); + } + + /// + /// Disposes of this object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing) + { + lock (Timer) + Timer.Dispose(); + } + } + } + + /// + /// Writes the bytes neccessary for a reliable send and stores the send. + /// + /// The byte array to write to. + /// The callback to make once the packet has been acknowledged. + void WriteReliableSendHeader(byte[] bytes, Action ackCallback) + { + lock (reliableDataPacketsSent) + { + //Find an ID not used yet. + ushort id; + + do + id = ++lastIDAllocated; + while (reliableDataPacketsSent.ContainsKey(id)); + + //Write ID + bytes[1] = (byte)((id >> 8) & 0xFF); + bytes[2] = (byte)id; + + //Create packet object + Packet packet = Packet.GetObject(); + packet.Set( + bytes, + (Packet p) => + { + //Double packet timeout + lock (p.Timer) + { + if (!p.Acknowledged) + { + p.Timer.Change(p.LastTimeout *= 2, Timeout.Infinite); + if (++p.Retransmissions > ResendsBeforeDisconnect) + { + HandleDisconnect(); + p.Recycle(); + return; + } + } + } + + WriteBytesToConnection(p.Data); + + Trace.WriteLine("Resend."); + }, + resendTimeout, + ackCallback + ); + + //Remember packet + reliableDataPacketsSent.Add(id, packet); + } + } + + /// + /// Handles receives from reliable packets. + /// + /// The buffer containing the data. + /// Whether the packet was a new packet or not. + bool HandleReliableReceive(byte[] bytes) + { + //Get the ID form the packet + ushort id = (ushort)((bytes[1] << 8) + bytes[2]); + + //Send an acknowledgement + SendAck(bytes[1], bytes[2]); + + /* + * It gets a little complicated here (note the fact I'm actually using a multiline comment for once...) + * + * In a simple world if our data is greater than the last reliable packet received (reliableReceiveLast) + * then it is guaranteed to be a new packet, if it's not we can see if we are missing that packet (lookup + * in reliableDataPacketsMissing). + * + * --------rrl############# (1) + * + * (where --- are packets received already and #### are packets that will be counted as new) + * + * Unfortunately if id becomes greater than 65535 it will loop back to zero so we will add a pointer that + * specifies any packets with an id behind it are also new (overwritePointer). + * + * ####op----------rrl##### (2) + * + * ------rll#########op---- (3) + * + * Anything behind than the reliableReceiveLast pointer (but greater than the overwritePointer is either a + * missing packet or something we've already received so when we change the pointers we need to make sure + * we keep note of what hasn't been received yet (reliableDataPacketsMissing). + * + * So... + */ + + lock (reliableDataPacketsMissing) + { + //Calculate overwritePointer + ushort overwritePointer = (ushort)(reliableReceiveLast - 32768); + + //Calculate if it is a new packet by examining if it is within the range + bool isNew; + if (overwritePointer < reliableReceiveLast) + isNew = id > reliableReceiveLast || id <= overwritePointer; //Figure (2) + else + isNew = id > reliableReceiveLast && id <= overwritePointer; //Figure (3) + + //If it's new or we've not received anything yet + if (isNew || !hasReceivedSomething) + { + //Mark items between the most recent receive and the id received as missing + for (ushort i = (ushort)(reliableReceiveLast + 1); i < id; i++) + reliableDataPacketsMissing.Add(i); + + //Update the most recently received + reliableReceiveLast = id; + hasReceivedSomething = true; + } + + //Else it could be a missing packet + else + { + //See if we're missing it, else this packet is a duplicate as so we return false + if (reliableDataPacketsMissing.Contains(id)) + reliableDataPacketsMissing.Remove(id); + else + return false; + } + } + + return true; + } + + /// + /// Handles acknowledgement packets to us. + /// + /// The buffer containing the data. + void HandleAcknowledgement(byte[] bytes) + { + //Get ID + ushort id = (ushort)((bytes[1] << 8) + bytes[2]); + + lock (reliableDataPacketsSent) + { + //Dispose of timer and remove from dictionary + if (reliableDataPacketsSent.ContainsKey(id)) + { + Packet packet = reliableDataPacketsSent[id]; + + packet.Acknowledged = true; + + if (packet.AckCallback != null) + packet.AckCallback.Invoke(); + + packet.Recycle(); + + reliableDataPacketsSent.Remove(id); + } + } + } + + /// + /// Sends an acknowledgement for a packet given its identification bytes. + /// + /// The first identification byte. + /// The second identification byte. + internal void SendAck(byte byte1, byte byte2) + { + //Always reply with acknowledgement in order to stop the sender repeatedly sending it + WriteBytesToConnection( //TODO group acks together + new byte[] + { + (byte)SendOptionInternal.Acknowledgement, + byte1, + byte2 + } + ); + } + } +} diff --git a/Hazel/Udp/UdpConnection.cs b/Hazel/Udp/UdpConnection.cs new file mode 100644 index 0000000..98afd27 --- /dev/null +++ b/Hazel/Udp/UdpConnection.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Hazel.Udp +{ + /// + /// Represents a connection that uses the UDP protocol. + /// + /// + public abstract partial class UdpConnection : NetworkConnection + { + /// + /// Creates a new UdpConnection and initializes the keep alive timer. + /// + protected UdpConnection() + { + InitializeKeepAliveTimer(); + } + + /// + /// Writes the given bytes to the connection. + /// + /// The bytes to write. + protected abstract void WriteBytesToConnection(byte[] bytes); + + /// + /// + /// + /// + /// Udp connections can currently send messages using and + /// . Fragmented messages are not currently supported and will default to + /// until implemented. + /// + /// + public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption.None) + { + //Early check + if (State != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + + //Add header information and send + HandleSend(bytes, (byte)sendOption); + } + + /// + /// Handles the reliable/fragmented sending from this connection. + /// + /// The data being sent. + /// The specified as its byte value. + /// The callback to invoke when this packet is acknowledged. + /// The bytes that should actually be sent. + protected void HandleSend(byte[] data, byte sendOption, Action ackCallback = null) + { + byte[] bytes; + switch (sendOption) + { + //Handle reliable header + case (byte)SendOption.Reliable: + bytes = new byte[data.Length + 3]; + WriteReliableSendHeader(bytes, ackCallback); + break; + + //Handle hellos (ignore data) + case (byte)SendOptionInternal.Hello: + bytes = new byte[3]; + WriteReliableSendHeader(bytes, ackCallback); + break; + + default: + bytes = new byte[data.Length + 1]; + break; + } + + //Add message type + bytes[0] = sendOption; + + //Copy data into new array + Buffer.BlockCopy(data, 0, bytes, bytes.Length - data.Length, data.Length); + + //Inform keepalive not to send for a while + ResetKeepAliveTimer(); + + //Write to connection + WriteBytesToConnection(bytes); + + Statistics.LogSend(data.Length, bytes.Length); + } + + /// + /// Handles the receiving of data. + /// + /// The buffer containing the bytes received. + /// The number of bytes that were received. + /// The bytes of data received. + protected byte[] HandleReceive(byte[] buffer, int bytesReceived) + { + //Inform keepalive not to send for a while + ResetKeepAliveTimer(); + + int headerSize = 1; + switch (buffer[0]) + { + //Handle reliable receives + case (byte)SendOption.Reliable: + headerSize = 3; + + if (HandleReliableReceive(buffer) == false) + return null; + break; + + //Handle acknowledgments + case (byte)SendOptionInternal.Acknowledgement: + HandleAcknowledgement(buffer); + + return null; + + //We need to acknowledge hello messages so just use the same reliable receive + //method + case (byte)SendOptionInternal.Hello: + HandleReliableReceive(buffer); + + return null; + + case (byte)SendOptionInternal.Disconnect: + HandleDisconnect(); + + return null; + } + + byte[] dataBytes = new byte[bytesReceived - headerSize]; + Buffer.BlockCopy(buffer, headerSize, dataBytes, 0, dataBytes.Length); + + Statistics.LogReceive(dataBytes.Length, bytesReceived); + + return dataBytes; + } + + /// + /// Sends a hello packet to the remote endpoint. + /// + /// The callback to invoke when the hello packet is acknowledged. + protected void SendHello(Action acknowledgeCallback) + { + HandleSend(new byte[0], (byte)SendOptionInternal.Hello, acknowledgeCallback); + } + + /// + /// Called when the socket has been disconnected at the remote host. + /// + /// The exception if one was the cause. + protected abstract void HandleDisconnect(HazelException e = null); + + /// + /// Sends a disconnect message to the end point. + /// + protected void SendDisconnect() + { + HandleSend(new byte[0], (byte)SendOptionInternal.Disconnect); //TODO Should disconnect wait for an ack? + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + DisposeKeepAliveTimer(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/Udp/UdpConnectionListener.cs b/Hazel/Udp/UdpConnectionListener.cs new file mode 100644 index 0000000..4d10dcc --- /dev/null +++ b/Hazel/Udp/UdpConnectionListener.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel.Udp +{ + /// + /// Listens for new UDP connections and creates UdpConnections for them. + /// + /// + public class UdpConnectionListener : NetworkConnectionListener + { + /// + /// The socket listening for connections. + /// + Socket listener; + + /// + /// Buffer to store incoming data in. + /// + byte[] dataBuffer = new byte[ushort.MaxValue]; + + /// + /// The connections we currently hold + /// + Dictionary connections = new Dictionary(); + + /// + /// Creates a new ConnectionListener for the given , port and . + /// + /// The IPAddress to listen on. + /// The port to listen on. + /// The to listen with. + public UdpConnectionListener(IPAddress IPAddress, int port, IPMode mode = IPMode.IPv4AndIPv6) + { + this.IPAddress = IPAddress; + this.Port = port; + + if (mode == IPMode.IPv4) + this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + else + { + this.listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + this.listener.DualMode = true; + } + } + + /// + public override void Start() + { + try + { + lock (listener) + listener.Bind(new IPEndPoint(IPAddress, Port)); + } + catch (SocketException e) + { + throw new HazelException("Could not start listening as a SocketException occured", e); + } + + StartListeningForData(); + } + + /// + /// Instructs the listener to begin listening. + /// + void StartListeningForData() + { + EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); + + try + { + lock (listener) + listener.BeginReceiveFrom(dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ref remoteEP, ReadCallback, dataBuffer); + } + catch (ObjectDisposedException) + { + return; + } + } + + /// + /// Called when data has been received by the listener. + /// + /// The asyncronous operation's result. + void ReadCallback(IAsyncResult result) + { + int bytesReceived; + EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + + //End the receive operation + try + { + lock (listener) + bytesReceived = listener.EndReceiveFrom(result, ref remoteEndPoint); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + catch (SocketException e) + { + //Errrr... shit... + //Not exactly much we can do if we've got here + throw e; + } + + //Exit if no bytes read, we've closed. + if (bytesReceived == 0) + return; + + //Copy to new buffer + byte[] buffer = new byte[bytesReceived]; + Buffer.BlockCopy((byte[])result.AsyncState, 0, buffer, 0, bytesReceived); + + //Begin receiving again + StartListeningForData(); + + bool aware; + UdpServerConnection connection; + lock (connections) + { + aware = connections.ContainsKey(remoteEndPoint); + + //If we're aware of this connection use the one already + if (aware) + connection = connections[remoteEndPoint]; + + //If this is a new client then connect with them! + else + { + //Check for malformed connection attempts + if (buffer[0] != (byte)SendOptionInternal.Hello || buffer.Length != 3) + return; + + connection = new UdpServerConnection(this, remoteEndPoint); + connections.Add(remoteEndPoint, connection); + + //Then ping back an ack to make sure they're happy + connection.SendAck(buffer[1], buffer[2]); + } + } + + //And fire the corresponding event + if (aware) + connection.InvokeDataReceived(buffer); + else + InvokeNewConnection(connection); + } + + /// + /// Sends data from the listener socket. + /// + /// The bytes to send. + /// The endpoint to send to. + internal void SendData(byte[] bytes, EndPoint endPoint) + { + SocketAsyncEventArgs args = new SocketAsyncEventArgs(); + args.SetBuffer(bytes, 0, bytes.Length); + args.RemoteEndPoint = endPoint; + + try + { + lock (listener) + listener.SendToAsync(args); + } + catch (SocketException e) + { + throw new HazelException("Could not send data as a SocketException occured.", e); + } + catch (ObjectDisposedException) + { + //Keep alive timer probably ran, ignore + return; + } + } + + /// + /// Removes a virtual connection from the list. + /// + /// The endpoint of the virtual connection. + internal void RemoveConnectionTo(EndPoint endPoint) + { + lock (connections) + connections.Remove(endPoint); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (listener) + listener.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/Udp/UdpServerConnection.cs b/Hazel/Udp/UdpServerConnection.cs new file mode 100644 index 0000000..eca3c3e --- /dev/null +++ b/Hazel/Udp/UdpServerConnection.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel.Udp +{ + /// + /// Represents a servers's connection to a client that uses the UDP protocol. + /// + /// + sealed class UdpServerConnection : UdpConnection + { + /// + /// The connection listener that we use the socket of. + /// + /// + /// Udp server connections utilize the same socket in the listener for sends/receives, this is the listener that + /// created this connection and is hence the listener this conenction sends and receives via. + /// + public UdpConnectionListener Listener { get; private set; } + + /// + /// Lock object for the writing to the state of the connection. + /// + Object stateLock = new Object(); + + /// + /// Creates a UdpConnection for the virtual connection to the endpoint. + /// + /// The listener that created this connection. + /// The endpoint that we are connected to. + internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint) + : base() + { + this.Listener = listener; + this.RemoteEndPoint = endPoint; + this.EndPoint = new NetworkEndPoint(endPoint); + + State = ConnectionState.Connected; + } + + /// + protected override void WriteBytesToConnection(byte[] bytes) + { + lock (stateLock) + { + if (State != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + + Listener.SendData(bytes, RemoteEndPoint); + } + } + + /// + /// + /// This will always throw a HazelException. + /// + public override void Connect() + { + throw new HazelException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); + } + + /// + /// Called by the listener when we have data. + /// + /// + internal void InvokeDataReceived(byte[] buffer) + { + byte[] data = HandleReceive(buffer, buffer.Length); + + if (data != null) + InvokeDataReceived(data, (SendOption)buffer[0]); + } + + /// + protected override void HandleDisconnect(HazelException e = null) + { + bool invoke = false; + + lock (stateLock) + { + //Only invoke the disconnected event if we're not already disconnecting + if (State == ConnectionState.Connected) + { + State = ConnectionState.Disconnecting; + invoke = true; + } + } + + //Invoke event outide lock if need be + if (invoke) + { + InvokeDisconnected(e); + + Dispose(); + } + } + + /// + protected override void Dispose(bool disposing) + { + //Here we just need to inform the listener we no longer need data. + if (disposing) + { + //Send disconnect message if we're not already disconnecting + if (State == ConnectionState.Connected) + SendDisconnect(); + + lock (stateLock) + { + Listener.RemoveConnectionTo(RemoteEndPoint); + + State = ConnectionState.NotConnected; + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/UdpClientConnection.cs b/Hazel/UdpClientConnection.cs deleted file mode 100644 index 7052b73..0000000 --- a/Hazel/UdpClientConnection.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Hazel -{ - /// - /// Represents a client's connection to a server that uses the UDP protocol. - /// - /// - public sealed class UdpClientConnection : UdpConnection - { - /// - /// The socket we're connected via. - /// - Socket socket; - - /// - /// The lock for the socket. - /// - Object socketLock = new Object(); - - /// - /// The buffer to store incomming data in. - /// - byte[] dataBuffer = new byte[ushort.MaxValue]; - - /// - /// Creates a new UdpClientConnection. - /// - public UdpClientConnection() - : base() - { - - } - - /// - protected override void WriteBytesToConnection(byte[] bytes) - { - //Pack - SocketAsyncEventArgs args = new SocketAsyncEventArgs(); - args.SetBuffer(bytes, 0, bytes.Length); - args.RemoteEndPoint = RemoteEndPoint; - - lock (socketLock) - { - if (State != ConnectionState.Connected && State != ConnectionState.Connecting) - throw new InvalidOperationException("Could not send data as this Connection is not connected and is not connecting. Did you disconnect?"); - - try - { - socket.SendToAsync(args); - } - catch (ObjectDisposedException) - { - //User probably called Disconnect in between this method starting and here so report the issue - throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); - } - catch (SocketException e) - { - HazelException he = new HazelException("Could not send data as a SocketException occured.", e); - HandleDisconnect(he); - throw he; - } - } - } - - /// - /// A to connect to. - public override void Connect(ConnectionEndPoint remoteEndPoint) - { - NetworkEndPoint nep = remoteEndPoint as NetworkEndPoint; - if (nep == null) - { - throw new ArgumentException("The remote end point of a UDP connection must be a NetworkEndPoint."); - } - - lock (socketLock) - { - this.EndPoint = nep; - this.RemoteEndPoint = nep.EndPoint; - - if (nep.IPMode == IPMode.IPv4) - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - else - { - socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - socket.DualMode = true; - } - - if (State != ConnectionState.NotConnected) - throw new InvalidOperationException("Cannot connect as the Connection is already connected."); - - State = ConnectionState.Connecting; - - //Begin listening - try - { - socket.Bind(new IPEndPoint(IPAddress.Any, 0)); - } - catch (SocketException e) - { - throw new HazelException("A socket exception occured while binding to the port.", e); - } - - try - { - StartListeningForData(); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there but make sure we're in NotConnected state. - //If we end up here I'm really lost... - State = ConnectionState.NotConnected; - return; - } - catch (SocketException e) - { - throw new HazelException("A Socket exception occured while initiating a receive operation.", e); - } - } - - //Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present) - //When acknowledged set the state to connected - SendHello(() => { lock (socketLock) State = ConnectionState.Connected; }); - - //Wait till hello packet is acknowledged and the state is set to Connected - WaitOnConnect(); - } - - /// - /// Instructs the listener to begin listening. - /// - void StartListeningForData() - { - lock (socketLock) - socket.BeginReceive(dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ReadCallback, dataBuffer); - } - - /// - /// Called when data has been received by the socket. - /// - /// The asyncronous operation's result. - void ReadCallback(IAsyncResult result) - { - int bytesReceived; - - //End the receive operation - try - { - lock (socketLock) - bytesReceived = socket.EndReceive(result); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there. - return; - } - catch (SocketException e) - { - HandleDisconnect(new HazelException("A socket exception occured while reading data.", e)); - return; - } - - //Exit if no bytes read, we've failed. - if (bytesReceived == 0) - { - HandleDisconnect(); - return; - } - - //Decode the data received - byte[] buffer = HandleReceive(dataBuffer, bytesReceived); - SendOption sendOption = (SendOption)dataBuffer[0]; - - //TODO may get better performance with Handle receive after and block copy call added - - //Begin receiving again - try - { - StartListeningForData(); - } - catch (SocketException e) - { - HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there. - return; - } - - if (buffer != null) - InvokeDataReceived(buffer, sendOption); - } - - /// - protected override void HandleDisconnect(HazelException e = null) - { - bool invoke = false; - - lock (socketLock) - { - //Only invoke the disconnected event if we're not already disconnecting - if (State == ConnectionState.Connected) - { - State = ConnectionState.Disconnecting; - invoke = true; - } - } - - //Invoke event outide lock if need be - if (invoke) - { - InvokeDisconnected(e); - - Dispose(); - } - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - //Send disconnect message if we're not already disconnecting - if (State == ConnectionState.Connected) - SendDisconnect(); - - //Dispose of the socket - lock (socketLock) - { - State = ConnectionState.NotConnected; - - socket.Dispose(); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Hazel/UdpConnection.KeepAlive.cs b/Hazel/UdpConnection.KeepAlive.cs deleted file mode 100644 index e9ab122..0000000 --- a/Hazel/UdpConnection.KeepAlive.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Hazel -{ - partial class UdpConnection - { - /// - /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds. - /// - /// - /// - /// Keepalive packets serve to close connections when an endpoint abruptly disconnects and to ensure than any - /// NAT devices do not close their translation for our argument. By ensuring there is regular contact the - /// connection can detect and prevent these issues. - /// - /// - /// The default value is 10 seconds, set to System.Threading.Timeout.Infinite to disable keepalive packets. - /// - /// - public int KeepAliveInterval - { - get - { - return keepAliveInterval; - } - - set - { - keepAliveInterval = value; - - //Update timer - ResetKeepAliveTimer(); - } - } - int keepAliveInterval = 10000; - - /// - /// The timer creating keepalive pulses. - /// - Timer keepAliveTimer; - - /// - /// Lock for keep alive timer. - /// - Object keepAliveTimerLock = new Object(); - - /// - /// Has the keep alive timer been disposed already? - /// - bool keepAliveTimerDisposed; - - /// - /// Starts the keepalive timer. - /// - void InitializeKeepAliveTimer() - { - lock (keepAliveTimerLock) - { - keepAliveTimer = new Timer( - (o) => - { - Trace.WriteLine("Keepalive packet sent."); - SendHello(null); - }, - null, - keepAliveInterval, - keepAliveInterval - ); - } - } - - /// - /// Resets the keepalive timer to zero. - /// - void ResetKeepAliveTimer() - { - lock (keepAliveTimerLock) - keepAliveTimer.Change(keepAliveInterval, keepAliveInterval); - } - - /// - /// Disposes of the keep alive timer. - /// - void DisposeKeepAliveTimer() - { - lock(keepAliveTimerLock) - { - if (!keepAliveTimerDisposed) - keepAliveTimer.Dispose(); - keepAliveTimerDisposed = true; - } - } - } -} diff --git a/Hazel/UdpConnection.Reliable.cs b/Hazel/UdpConnection.Reliable.cs deleted file mode 100644 index 3c3cf64..0000000 --- a/Hazel/UdpConnection.Reliable.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Hazel -{ - partial class UdpConnection - { - /// - /// The starting timeout, in miliseconds, at which data will be resent. - /// - /// - /// For reliable delivery data is resent at specified intervals unless an acknowledgement is received from the - /// receiving device. The ResendTimeout specifies the interval between the packets being resent, each time a packet - /// is resent the interval is doubled for that packet until the number of resends exceeds the - /// value. - /// - public int ResendTimeout { get { return resendTimeout; } set { resendTimeout = value; } } - private volatile int resendTimeout = 200; //TODO this based of average ping? - - /// - /// Holds the last ID allocated. - /// - volatile ushort lastIDAllocated; - - /// - /// The packets of data that have been transmitted reliably and not acknowledged. - /// - Dictionary reliableDataPacketsSent = new Dictionary(); - - /// - /// The last packets that were received. - /// - HashSet reliableDataPacketsMissing = new HashSet(); - - /// - /// The packet id that was received last. - /// - volatile ushort reliableReceiveLast = 0; - - /// - /// Has the connection received anything yet - /// - volatile bool hasReceivedSomething = false; - - /// - /// The maximum times a message should be resent before marking the endpoint as disconnected. - /// - /// - /// Reliable packets will be resent at an interval defined in for the number of times - /// specified here. Once a packet has been retransmitted this number of times and has not been acknowledged the - /// connection will be marked as disconnected and the Disconnected event - /// will be invoked. - /// - public int ResendsBeforeDisconnect { get { return resendsBeforeDisconnect; } set { resendsBeforeDisconnect = value; } } - private volatile int resendsBeforeDisconnect = 3; - - /// - /// Class to hold packet data - /// - class Packet : IRecyclable, IDisposable - { - /// - /// Object pool for this event. - /// - static readonly ObjectPool objectPool = new ObjectPool(() => new Packet()); - - /// - /// Returns an instance of this object from the pool. - /// - /// - internal static Packet GetObject() - { - return objectPool.GetObject(); - } - - public byte[] Data; - public Timer Timer; - public volatile int LastTimeout; - public Action AckCallback; - public volatile bool Acknowledged; - public volatile int Retransmissions; - - Packet() - { - - } - - internal void Set(byte[] data, Action resendAction, int timeout, Action ackCallback) - { - Data = data; - - Timer = new Timer( - (object obj) => resendAction(this), - null, - timeout, - Timeout.Infinite - ); - - LastTimeout = timeout; - AckCallback = ackCallback; - Acknowledged = false; - Retransmissions = 0; - } - - /// - /// Returns this object back to the object pool from whence it came. - /// - public void Recycle() - { - lock (Timer) - Timer.Dispose(); - - objectPool.PutObject(this); - } - - /// - /// Disposes of this object. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - if (disposing) - { - lock (Timer) - Timer.Dispose(); - } - } - } - - /// - /// Writes the bytes neccessary for a reliable send and stores the send. - /// - /// The byte array to write to. - void WriteReliableSendHeader(byte[] bytes, Action ackCallback) - { - lock (reliableDataPacketsSent) - { - //Find an ID not used yet. - ushort id; - - do - id = ++lastIDAllocated; - while (reliableDataPacketsSent.ContainsKey(id)); - - //Write ID - bytes[1] = (byte)((id >> 8) & 0xFF); - bytes[2] = (byte)id; - - //Create packet object - Packet packet = Packet.GetObject(); - packet.Set( - bytes, - (Packet p) => - { - //Double packet timeout - lock (p.Timer) - { - if (!p.Acknowledged) - { - p.Timer.Change(p.LastTimeout *= 2, Timeout.Infinite); - if (++p.Retransmissions > ResendsBeforeDisconnect) - { - HandleDisconnect(); - p.Recycle(); - return; - } - } - } - - WriteBytesToConnection(p.Data); - - Trace.WriteLine("Resend."); - }, - resendTimeout, - ackCallback - ); - - //Remember packet - reliableDataPacketsSent.Add(id, packet); - } - } - - /// - /// Handles receives from reliable packets. - /// - /// The buffer containing the data. - /// Whether the packet was a new packet or not. - bool HandleReliableReceive(byte[] bytes) - { - //Get the ID form the packet - ushort id = (ushort)((bytes[1] << 8) + bytes[2]); - - //Send an acknowledgement - SendAck(bytes[1], bytes[2]); - - /* - * It gets a little complicated here (note the fact I'm actually using a multiline comment for once...) - * - * In a simple world if our data is greater than the last reliable packet received (reliableReceiveLast) - * then it is guaranteed to be a new packet, if it's not we can see if we are missing that packet (lookup - * in reliableDataPacketsMissing). - * - * --------rrl############# (1) - * - * (where --- are packets received already and #### are packets that will be counted as new) - * - * Unfortunately if id becomes greater than 65535 it will loop back to zero so we will add a pointer that - * specifies any packets with an id behind it are also new (overwritePointer). - * - * ####op----------rrl##### (2) - * - * ------rll#########op---- (3) - * - * Anything behind than the reliableReceiveLast pointer (but greater than the overwritePointer is either a - * missing packet or something we've already received so when we change the pointers we need to make sure - * we keep note of what hasn't been received yet (reliableDataPacketsMissing). - * - * So... - */ - - lock (reliableDataPacketsMissing) - { - //Calculate overwritePointer - ushort overwritePointer = (ushort)(reliableReceiveLast - 32768); - - //Calculate if it is a new packet by examining if it is within the range - bool isNew; - if (overwritePointer < reliableReceiveLast) - isNew = id > reliableReceiveLast || id <= overwritePointer; //Figure (2) - else - isNew = id > reliableReceiveLast && id <= overwritePointer; //Figure (3) - - //If it's new or we've not received anything yet - if (isNew || !hasReceivedSomething) - { - //Mark items between the most recent receive and the id received as missing - for (ushort i = (ushort)(reliableReceiveLast + 1); i < id; i++) - reliableDataPacketsMissing.Add(i); - - //Update the most recently received - reliableReceiveLast = id; - hasReceivedSomething = true; - } - - //Else it could be a missing packet - else - { - //See if we're missing it, else this packet is a duplicate as so we return false - if (reliableDataPacketsMissing.Contains(id)) - reliableDataPacketsMissing.Remove(id); - else - return false; - } - } - - return true; - } - - /// - /// Handles acknowledgement packets to us. - /// - /// The buffer containing the data. - void HandleAcknowledgement(byte[] bytes) - { - //Get ID - ushort id = (ushort)((bytes[1] << 8) + bytes[2]); - - lock (reliableDataPacketsSent) - { - //Dispose of timer and remove from dictionary - if (reliableDataPacketsSent.ContainsKey(id)) - { - Packet packet = reliableDataPacketsSent[id]; - - packet.Acknowledged = true; - - if (packet.AckCallback != null) - packet.AckCallback.Invoke(); - - packet.Recycle(); - - reliableDataPacketsSent.Remove(id); - } - } - } - - /// - /// Sends an acknowledgement for a packet given its identification bytes. - /// - /// The first identification byte. - /// The second identification byte. - internal void SendAck(byte byte1, byte byte2) - { - //Always reply with acknowledgement in order to stop the sender repeatedly sending it - WriteBytesToConnection( //TODO group acks together - new byte[] - { - (byte)SendOptionInternal.Acknowledgement, - byte1, - byte2 - } - ); - } - } -} diff --git a/Hazel/UdpConnection.cs b/Hazel/UdpConnection.cs deleted file mode 100644 index 8890c92..0000000 --- a/Hazel/UdpConnection.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; - -namespace Hazel -{ - /// - /// Represents a connection that uses the UDP protocol. - /// - /// - public abstract partial class UdpConnection : NetworkConnection - { - /// - /// Creates a new UdpConnection and initializes the keep alive timer. - /// - protected UdpConnection() - { - InitializeKeepAliveTimer(); - } - - /// - /// Writes the given bytes to the connection. - /// - /// The bytes to write. - protected abstract void WriteBytesToConnection(byte[] bytes); - - /// - /// - /// - /// - /// Udp connections can currently send messages using and - /// . Fragmented messages are not currently supported and will default to - /// until implemented. - /// - /// - public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption.None) - { - //Early check - if (State != ConnectionState.Connected) - throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); - - //Add header information and send - HandleSend(bytes, (byte)sendOption); - } - - /// - /// Handles the reliable/fragmented sending from this connection. - /// - /// The data being sent. - /// The specified as its byte value. - /// The callback to invoke when this packet is acknowledged. - /// The bytes that should actually be sent. - protected void HandleSend(byte[] data, byte sendOption, Action ackCallback = null) - { - byte[] bytes; - switch (sendOption) - { - //Handle reliable header - case (byte)SendOption.Reliable: - bytes = new byte[data.Length + 3]; - WriteReliableSendHeader(bytes, ackCallback); - break; - - //Handle hellos (ignore data) - case (byte)SendOptionInternal.Hello: - bytes = new byte[3]; - WriteReliableSendHeader(bytes, ackCallback); - break; - - default: - bytes = new byte[data.Length + 1]; - break; - } - - //Add message type - bytes[0] = sendOption; - - //Copy data into new array - Buffer.BlockCopy(data, 0, bytes, bytes.Length - data.Length, data.Length); - - //Inform keepalive not to send for a while - ResetKeepAliveTimer(); - - //Write to connection - WriteBytesToConnection(bytes); - - Statistics.LogSend(data.Length, bytes.Length); - } - - /// - /// Handles the receiving of data. - /// - /// The buffer containing the bytes received. - /// The number of bytes that were received. - /// The bytes of data received. - protected byte[] HandleReceive(byte[] buffer, int bytesReceived) - { - //Inform keepalive not to send for a while - ResetKeepAliveTimer(); - - int headerSize = 1; - switch (buffer[0]) - { - //Handle reliable receives - case (byte)SendOption.Reliable: - headerSize = 3; - - if (HandleReliableReceive(buffer) == false) - return null; - break; - - //Handle acknowledgments - case (byte)SendOptionInternal.Acknowledgement: - HandleAcknowledgement(buffer); - - return null; - - //We need to acknowledge hello messages so just use the same reliable receive - //method - case (byte)SendOptionInternal.Hello: - HandleReliableReceive(buffer); - - return null; - - case (byte)SendOptionInternal.Disconnect: - HandleDisconnect(); - - return null; - } - - byte[] dataBytes = new byte[bytesReceived - headerSize]; - Buffer.BlockCopy(buffer, headerSize, dataBytes, 0, dataBytes.Length); - - Statistics.LogReceive(dataBytes.Length, bytesReceived); - - return dataBytes; - } - - /// - /// Sends a hello packet to the remote endpoint. - /// - /// The callback to invoke when the hello packet is acknowledged. - protected void SendHello(Action acknowledgeCallback) - { - HandleSend(new byte[0], (byte)SendOptionInternal.Hello, acknowledgeCallback); - } - - /// - /// Called when the socket has been disconnected at the remote host. - /// - /// The exception if one was the cause. - protected abstract void HandleDisconnect(HazelException e = null); - - /// - /// Sends a disconnect message to the end point. - /// - protected void SendDisconnect() - { - HandleSend(new byte[0], (byte)SendOptionInternal.Disconnect); //TODO Should disconnect wait for an ack? - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - DisposeKeepAliveTimer(); - } - - base.Dispose(disposing); - } - } -} diff --git a/Hazel/UdpConnectionListener.cs b/Hazel/UdpConnectionListener.cs deleted file mode 100644 index 0572b61..0000000 --- a/Hazel/UdpConnectionListener.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; - -namespace Hazel -{ - /// - /// Listens for new UDP connections and creates UdpConnections for them. - /// - /// - public class UdpConnectionListener : NetworkConnectionListener - { - /// - /// The socket listening for connections. - /// - Socket listener; - - /// - /// Buffer to store incoming data in. - /// - byte[] dataBuffer = new byte[ushort.MaxValue]; - - /// - /// The connections we currently hold - /// - Dictionary connections = new Dictionary(); - - /// - /// Creates a new ConnectionListener for the given , port and . - /// - /// The IPAddress to listen on. - /// The port to listen on. - /// The to listen with. - public UdpConnectionListener(IPAddress IPAddress, int port, IPMode mode = IPMode.IPv4AndIPv6) - { - this.IPAddress = IPAddress; - this.Port = port; - - if (mode == IPMode.IPv4) - this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - else - { - this.listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - this.listener.DualMode = true; - } - } - - /// - public override void Start() - { - try - { - lock (listener) - listener.Bind(new IPEndPoint(IPAddress, Port)); - } - catch (SocketException e) - { - throw new HazelException("Could not start listening as a SocketException occured", e); - } - - StartListeningForData(); - } - - /// - /// Instructs the listener to begin listening. - /// - void StartListeningForData() - { - EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); - - try - { - lock (listener) - listener.BeginReceiveFrom(dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ref remoteEP, ReadCallback, dataBuffer); - } - catch (ObjectDisposedException) - { - return; - } - } - - /// - /// Called when data has been received by the listener. - /// - /// The asyncronous operation's result. - void ReadCallback(IAsyncResult result) - { - int bytesReceived; - EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); - - //End the receive operation - try - { - lock (listener) - bytesReceived = listener.EndReceiveFrom(result, ref remoteEndPoint); - } - catch (ObjectDisposedException) - { - //If the socket's been disposed then we can just end there. - return; - } - catch (SocketException e) - { - //Errrr... shit... - //Not exactly much we can do if we've got here - throw e; - } - - //Exit if no bytes read, we've closed. - if (bytesReceived == 0) - return; - - //Copy to new buffer - byte[] buffer = new byte[bytesReceived]; - Buffer.BlockCopy((byte[])result.AsyncState, 0, buffer, 0, bytesReceived); - - //Begin receiving again - StartListeningForData(); - - bool aware; - UdpServerConnection connection; - lock (connections) - { - aware = connections.ContainsKey(remoteEndPoint); - - //If we're aware of this connection use the one already - if (aware) - connection = connections[remoteEndPoint]; - - //If this is a new client then connect with them! - else - { - //Check for malformed connection attempts - if (buffer[0] != (byte)SendOptionInternal.Hello || buffer.Length != 3) - return; - - connection = new UdpServerConnection(this, remoteEndPoint); - connections.Add(remoteEndPoint, connection); - - //Then ping back an ack to make sure they're happy - connection.SendAck(buffer[1], buffer[2]); - } - } - - //And fire the corresponding event - if (aware) - connection.InvokeDataReceived(buffer); - else - InvokeNewConnection(connection); - } - - /// - /// Sends data from the listener socket. - /// - /// The bytes to send. - /// The endpoint to send to. - internal void SendData(byte[] bytes, EndPoint endPoint) - { - SocketAsyncEventArgs args = new SocketAsyncEventArgs(); - args.SetBuffer(bytes, 0, bytes.Length); - args.RemoteEndPoint = endPoint; - - try - { - lock (listener) - listener.SendToAsync(args); - } - catch (SocketException e) - { - throw new HazelException("Could not send data as a SocketException occured.", e); - } - catch (ObjectDisposedException) - { - //Keep alive timer probably ran, ignore - return; - } - } - - /// - /// Removes a virtual connection from the list. - /// - /// The endpoint of the virtual connection. - internal void RemoveConnectionTo(EndPoint endPoint) - { - lock (connections) - connections.Remove(endPoint); - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - lock (listener) - listener.Dispose(); - } - - base.Dispose(disposing); - } - } -} diff --git a/Hazel/UdpServerConnection.cs b/Hazel/UdpServerConnection.cs deleted file mode 100644 index 7940913..0000000 --- a/Hazel/UdpServerConnection.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace Hazel -{ - /// - /// Represents a servers's connection to a client that uses the UDP protocol. - /// - /// - sealed class UdpServerConnection : UdpConnection - { - /// - /// The connection listener that we use the socket of. - /// - /// - /// Udp server connections utilize the same socket in the listener for sends/receives, this is the listener that - /// created this connection and is hence the listener this conenction sends and receives via. - /// - public UdpConnectionListener Listener { get; private set; } - - /// - /// Lock object for the writing to the state of the connection. - /// - Object stateLock = new Object(); - - /// - /// Creates a UdpConnection for the virtual connection to the endpoint. - /// - /// The listener that created this connection. - /// The endpoint that we are connected to. - internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint) - : base() - { - this.Listener = listener; - this.RemoteEndPoint = endPoint; - this.EndPoint = new NetworkEndPoint(endPoint); - - State = ConnectionState.Connected; - } - - /// - protected override void WriteBytesToConnection(byte[] bytes) - { - lock (stateLock) - { - if (State != ConnectionState.Connected) - throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); - - Listener.SendData(bytes, RemoteEndPoint); - } - } - - /// - /// - /// This will always throw an InvalidOperationException. - /// - public override void Connect(ConnectionEndPoint remoteEndPoint) - { - throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); - } - - /// - /// Called by the listener when we have data. - /// - /// - internal void InvokeDataReceived(byte[] buffer) - { - byte[] data = HandleReceive(buffer, buffer.Length); - - if (data != null) - InvokeDataReceived(data, (SendOption)buffer[0]); - } - - /// - protected override void HandleDisconnect(HazelException e = null) - { - bool invoke = false; - - lock (stateLock) - { - //Only invoke the disconnected event if we're not already disconnecting - if (State == ConnectionState.Connected) - { - State = ConnectionState.Disconnecting; - invoke = true; - } - } - - //Invoke event outide lock if need be - if (invoke) - { - InvokeDisconnected(e); - - Dispose(); - } - } - - /// - protected override void Dispose(bool disposing) - { - //Here we just need to inform the listener we no longer need data. - if (disposing) - { - //Send disconnect message if we're not already disconnecting - if (State == ConnectionState.Connected) - SendDisconnect(); - - lock (stateLock) - { - Listener.RemoveConnectionTo(RemoteEndPoint); - - State = ConnectionState.NotConnected; - } - } - - base.Dispose(disposing); - } - } -}