From: JamJar00 Date: Sun, 3 Apr 2016 16:08:59 +0000 (+0100) Subject: Initial Commit X-Git-Tag: 1.0.0~168 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=012f5c536fa9a55388a6184ce9b70ea31b62e8cc;p=rhonda%2Fimpostor.hazel.git Initial Commit --- diff --git a/Hazel.UnitTests/Hazel.UnitTests.csproj b/Hazel.UnitTests/Hazel.UnitTests.csproj new file mode 100644 index 0000000..62520fd --- /dev/null +++ b/Hazel.UnitTests/Hazel.UnitTests.csproj @@ -0,0 +1,91 @@ + + + + Debug + AnyCPU + {1394E4CA-E17A-42F5-9216-8046ACA8D16B} + Library + Properties + Hazel.UnitTests + Hazel.UnitTests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + {02cfbd30-d77d-400f-94b2-700f60efdd7f} + Hazel + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/Hazel.UnitTests/Properties/AssemblyInfo.cs b/Hazel.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c6e5128 --- /dev/null +++ b/Hazel.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Hazel.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Hazel.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e88c2226-946e-4f00-9336-8d8d7946ac23")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Hazel.UnitTests/TcpConnectionTests.cs b/Hazel.UnitTests/TcpConnectionTests.cs new file mode 100644 index 0000000..cd0bdf5 --- /dev/null +++ b/Hazel.UnitTests/TcpConnectionTests.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; + +namespace Hazel.UnitTests +{ + [TestClass] + public class TcpConnectionTests + { + /// + /// Tests the fields on TcpConnection. + /// + [TestMethod] + public void TcpConnectionFieldTest() + { + using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) + using (TcpConnection connection = new TcpConnection()) + { + listener.Start(); + + NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); + connection.Connect(ep); + + //Connection fields + Assert.AreEqual(ep, connection.EndPoint); + + //TcpConnection fields + Assert.AreEqual(new IPEndPoint(IPAddress.Loopback, 4296), connection.RemoteEndPoint); + Assert.AreEqual(0, connection.Statistics.DataBytesSent); + Assert.AreEqual(0, connection.Statistics.DataBytesReceived); + } + } + + /// + /// Tests sending and receiving on the TcpConnection. + /// + [TestMethod] + public void TcpConnectionSendReceiveTest() + { + using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296)) + using (TcpConnection connection = new TcpConnection()) + { + TestHelper.RunSendReceiveTest(listener, connection, 4, 0, 0); + } + } + } +} diff --git a/Hazel.UnitTests/TestHelper.cs b/Hazel.UnitTests/TestHelper.cs new file mode 100644 index 0000000..3e53b0c --- /dev/null +++ b/Hazel.UnitTests/TestHelper.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Hazel; +using System.Net; +using System.Threading; +using System.Diagnostics; + +namespace Hazel.UnitTests +{ + [TestClass] + public static class TestHelper + { + /// + /// Runs a general test on the given listener and connection. + /// + /// The listener to test. + /// The connection to test. + internal static void RunSendReceiveTest(ConnectionListener listener, Connection connection, int headerSize, int handshakeSize, int totalHandshakeSize) + { + //Setup meta stuff + byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + AutoResetEvent mutex = new AutoResetEvent(false); + + //Setup listener + listener.NewConnection += delegate(object sender, NewConnectionEventArgs args) + { + args.Connection.WriteBytes(data); + Assert.AreEqual(data.Length, args.Connection.Statistics.DataBytesSent); + Assert.AreEqual(0, args.Connection.Statistics.DataBytesReceived); + Assert.AreEqual(data.Length + headerSize, args.Connection.Statistics.TotalBytesSent); + Assert.AreEqual(0, args.Connection.Statistics.TotalBytesReceived); + }; + + listener.Start(); + + //Setup conneciton + connection.DataReceived += delegate(object sender, DataEventArgs args) + { + Trace.WriteLine("Data was received correctly."); + + for (int i = 0; i < data.Length; i++) + { + Assert.AreEqual(data[i], args.Bytes[i]); + } + + mutex.Set(); + }; + + connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296)); + + //Wait until data is received + mutex.WaitOne(); + + Assert.AreEqual(handshakeSize, connection.Statistics.DataBytesSent); + Assert.AreEqual(data.Length, connection.Statistics.DataBytesReceived); + Assert.AreEqual(totalHandshakeSize, connection.Statistics.TotalBytesSent); + Assert.AreEqual(data.Length + headerSize, connection.Statistics.TotalBytesReceived); + } + } +} diff --git a/Hazel.UnitTests/UdpConnectionTests.cs b/Hazel.UnitTests/UdpConnectionTests.cs new file mode 100644 index 0000000..13b733f --- /dev/null +++ b/Hazel.UnitTests/UdpConnectionTests.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; + +namespace Hazel.UnitTests +{ + [TestClass] + public class UdpConnectionTests + { + /// + /// Tests the fields on UdpConnection. + /// + [TestMethod] + public void UdpConnectionFieldTest() + { + using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) + using (UdpConnection connection = new UdpClientConnection()) + { + listener.Start(); + + NetworkEndPoint ep = new NetworkEndPoint(IPAddress.Loopback, 4296); + connection.Connect(ep); + + //Connection fields + Assert.AreEqual(ep, connection.EndPoint); + + //UdpConnection fields + Assert.AreEqual(new IPEndPoint(IPAddress.Loopback, 4296), connection.RemoteEndPoint); + Assert.AreEqual(1, connection.Statistics.DataBytesSent); + Assert.AreEqual(0, connection.Statistics.DataBytesReceived); + } + } + + /// + /// Tests sending and receiving on the UdpConnection. + /// + [TestMethod] + public void UdpConnectionSendReceiveTest() + { + using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296)) + using (UdpConnection connection = new UdpClientConnection()) + { + TestHelper.RunSendReceiveTest(listener, connection, 1, 1, 2); + } + } + } +} diff --git a/Hazel.sln b/Hazel.sln new file mode 100644 index 0000000..02013f7 --- /dev/null +++ b/Hazel.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hazel", "Hazel\Hazel.csproj", "{02CFBD30-D77D-400F-94B2-700F60EFDD7F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hazel.UnitTests", "Hazel.UnitTests\Hazel.UnitTests.csproj", "{1394E4CA-E17A-42F5-9216-8046ACA8D16B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {02CFBD30-D77D-400F-94B2-700F60EFDD7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02CFBD30-D77D-400F-94B2-700F60EFDD7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02CFBD30-D77D-400F-94B2-700F60EFDD7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02CFBD30-D77D-400F-94B2-700F60EFDD7F}.Release|Any CPU.Build.0 = Release|Any CPU + {1394E4CA-E17A-42F5-9216-8046ACA8D16B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1394E4CA-E17A-42F5-9216-8046ACA8D16B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1394E4CA-E17A-42F5-9216-8046ACA8D16B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1394E4CA-E17A-42F5-9216-8046ACA8D16B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Hazel/Connection.cs b/Hazel/Connection.cs new file mode 100644 index 0000000..7350be6 --- /dev/null +++ b/Hazel/Connection.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net.Sockets; +using System.Net; + + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + /// + /// Handles the sending and receiving of messages through the channel to give connection orientated, packet based transmission. + /// + public abstract class Connection : IDisposable + { + /// + /// Called when a message has been received. + /// + public event EventHandler DataReceived; + + /// + /// Called when the end point disconnects from us or an error occurs. + /// + public event EventHandler Disconnected; + + /// + /// The end point of this Connection. + /// + public ConnectionEndPoint EndPoint { get; protected set; } + + /// + /// The traffic statistics about this Connection. + /// + public ConnectionStatistics Statistics { get; protected set; } + + /// + /// The state of this connection. + /// + public ConnectionState State { get { return state; } protected set { state = value; } } + volatile ConnectionState state; + + /// + /// Constructor that initializes the ConnecitonStatistics object. + /// + protected Connection() + { + Statistics = new ConnectionStatistics(); + + State = ConnectionState.NotConnected; + } + + /// + /// Writes an array of bytes to the connection and prefixes the length. + /// + /// The bytes of the message to send. + /// The options this data is requested to send with. + /// + /// The sendOptions parameter is only a request to use those options and the actual method used to send the + /// data is up to the implementation. There are circumstances where this parameter may be ignored but in + /// general any implementer should aim to always follow the user's request here. + /// + public abstract void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None); + + /// + /// Connects the connection to a remote server and begins listening. + /// + public abstract void Connect(ConnectionEndPoint remoteEndPoint); + + /// + /// Invokes the DataReceived event to alert subscribers we received data. + /// + /// The arguments to supply. + protected void InvokeDataReceived(DataEventArgs args) + { + //Make a copy to avoid race condition between null check and invocation + EventHandler handler = DataReceived; + if (handler != null) + handler(this, args); + } + + /// + /// Invokes the Disconnected event to alert hooked up methods there was an error or the remote end point disconnected. + /// + /// The arguments to supply. + protected void InvokeDisconnected(DisconnectedEventArgs args) + { + //Make a copy to avoid race condition between null check and invocation + EventHandler handler = Disconnected; + if (handler != null) + handler(this, args); + } + + /// + /// Closes this connections safely. + /// + public void Close() + { + Dispose(); + } + + /// + /// Disposes of this NetworkConnection. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of this NetworkConnection. + /// + /// Are we currently disposing? + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + } +} diff --git a/Hazel/ConnectionEndPoint.cs b/Hazel/ConnectionEndPoint.cs new file mode 100644 index 0000000..20a358f --- /dev/null +++ b/Hazel/ConnectionEndPoint.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Hazel +{ + public abstract class ConnectionEndPoint + { + } +} diff --git a/Hazel/ConnectionListener.cs b/Hazel/ConnectionListener.cs new file mode 100644 index 0000000..640bf4c --- /dev/null +++ b/Hazel/ConnectionListener.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel +{ + /// + /// Base class for all connection listeners. + /// + public abstract class ConnectionListener : IDisposable + { + /// + /// Invoked when a new TCP connection is heard. + /// + public event EventHandler NewConnection; + + /// + /// Makes this connection listener begin listening for connections. + /// + public abstract void Start(); + + /// + /// Invokes the NewConnection event with the supplied args. + /// + /// The arguments for the event. + protected void FireNewConnectionEvent(NewConnectionEventArgs args) + { + //Make a copy to avoid race condition between null check and invocation + EventHandler handler = NewConnection; + if (handler != null) + handler(this, args); + } + + /// + /// Call to dispose of the connection listener. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Called when the object is being disposed. + /// + /// Are we disposing? + protected virtual void Dispose(bool disposing) + { + + } + } +} diff --git a/Hazel/ConnectionState.cs b/Hazel/ConnectionState.cs new file mode 100644 index 0000000..c8269bf --- /dev/null +++ b/Hazel/ConnectionState.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Hazel +{ + /// + /// Marks the state a Connection is currently in. + /// + public enum ConnectionState + { + /// + /// The Connection has either not been established yet or has been disconnected. + /// + NotConnected, + + /// + /// The Connection is currently connecting to an endpoint. + /// + Connecting, + + /// + /// The Connection is connected and data can be transfered. + /// + Connected, + + /// + /// The Connection is currently disconnecting. + /// + Disconnecting + } +} diff --git a/Hazel/ConnectionStatistics.cs b/Hazel/ConnectionStatistics.cs new file mode 100644 index 0000000..841c9d3 --- /dev/null +++ b/Hazel/ConnectionStatistics.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Hazel +{ + /// + /// Holds statistics about the traffic through a Connection. + /// + public class ConnectionStatistics + { + /// + /// The number of messages sent. + /// + public long MessagesSent + { + get + { + return Interlocked.Read(ref messagesSent); + } + } + + /// + /// The number of messages sent. + /// + long messagesSent; + + /// + /// The number of bytes of data sent. + /// + public long DataBytesSent + { + get + { + return Interlocked.Read(ref dataBytesSent); + } + } + + /// + /// The number of bytes of data sent. + /// + long dataBytesSent; + + /// + /// The number of bytes sent in total. + /// + public long TotalBytesSent + { + get + { + return Interlocked.Read(ref totalBytesSent); + } + } + + /// + /// The number of bytes sent in total. + /// + long totalBytesSent; + + /// + /// The number of messages received. + /// + public long MessagesReceived + { + get + { + return Interlocked.Read(ref messagesReceived); + } + } + + /// + /// The number of messages received. + /// + long messagesReceived; + + /// + /// The number of bytes of data received. + /// + public long DataBytesReceived + { + get + { + return Interlocked.Read(ref dataBytesReceived); + } + } + + /// + /// The number of bytes of data received. + /// + long dataBytesReceived; + + /// + /// The number of bytes received in total. + /// + public long TotalBytesReceived + { + get + { + return Interlocked.Read(ref totalBytesReceived); + } + } + + /// + /// The number of bytes received in total. + /// + long totalBytesReceived; + + /// + /// Logs the sending of a data packet in the statistics. + /// + /// The number of bytes of data sent. + /// The total number of bytes sent. + internal void LogSend(int dataLength, int totalLength) + { + Interlocked.Increment(ref messagesSent); + Interlocked.Add(ref dataBytesSent, dataLength); + Interlocked.Add(ref totalBytesSent, totalLength); + } + + /// + /// Logs the receiving of a data packet in the statistics. + /// + /// The number of bytes of data received. + /// The total number of bytes received. + internal void LogReceive(int dataLength, int totalLength) + { + Interlocked.Increment(ref messagesReceived); + Interlocked.Add(ref dataBytesReceived, dataLength); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + } +} diff --git a/Hazel/DataEventArgs.cs b/Hazel/DataEventArgs.cs new file mode 100644 index 0000000..a68e2fd --- /dev/null +++ b/Hazel/DataEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + public class DataEventArgs : EventArgs + { + /// + /// The bytes received. + /// + public byte[] Bytes; + + /// + /// Creates DataEventArgs from bytes received. + /// + /// + public DataEventArgs(byte[] bytes) + { + this.Bytes = bytes; + } + } +} diff --git a/Hazel/DisconnectedEventArgs.cs b/Hazel/DisconnectedEventArgs.cs new file mode 100644 index 0000000..c4b9a0b --- /dev/null +++ b/Hazel/DisconnectedEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Hazel +{ + /// + /// Events args for disconnected events. + /// + public class DisconnectedEventArgs + { + /// + /// The exception, if any, that caused the disconnect, otherwise null. + /// + public Exception Exception { get; private set; } + + /// + /// Creates a DisconnectedEventArgs from the given exception or null + /// + /// The exception if the cause. + internal DisconnectedEventArgs(Exception e) + { + this.Exception = e; + } + } +} diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj new file mode 100644 index 0000000..dcad336 --- /dev/null +++ b/Hazel/Hazel.csproj @@ -0,0 +1,82 @@ + + + + + Debug + AnyCPU + {02CFBD30-D77D-400F-94B2-700F60EFDD7F} + Library + Properties + Hazel + Hazel + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + Hazel.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + \ No newline at end of file diff --git a/Hazel/Hazel.snk b/Hazel/Hazel.snk new file mode 100644 index 0000000..3a4a4f0 Binary files /dev/null and b/Hazel/Hazel.snk differ diff --git a/Hazel/HazelException.cs b/Hazel/HazelException.cs new file mode 100644 index 0000000..86f157b --- /dev/null +++ b/Hazel/HazelException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Hazel +{ + /// + /// Wrapper for exceptions thrown from Hazel. + /// + class HazelException : Exception + { + internal HazelException(string msg) : base (msg) + { + + } + + internal HazelException(string msg, System.Net.Sockets.SocketException e) : base (msg, e) + { + + } + } +} diff --git a/Hazel/NetworkEndPoint.cs b/Hazel/NetworkEndPoint.cs new file mode 100644 index 0000000..66947d7 --- /dev/null +++ b/Hazel/NetworkEndPoint.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using System.Net; + +namespace Hazel +{ + /// + /// Represents an endpoint to a remote resource on a network. + /// + public class NetworkEndPoint : ConnectionEndPoint + { + /// + /// The EndPoint this points to. + /// + public EndPoint EndPoint { get; set; } + + /// + /// Creates a NetworkEndPoint from a given EndPoint. + /// + /// The endpoint we represent./param> + public NetworkEndPoint(EndPoint endPoint) + { + this.EndPoint = endPoint; + } + + /// + /// Create a NetworkEndPoint to the specified address and port. + /// + /// The IP address of the server. + /// The port the server is listening on. + public NetworkEndPoint(IPAddress address, int port) : this(new IPEndPoint(address, port)) + { + + } + + /// + /// Creates a NetworkEndPoint to the specified IP address and port. + /// + /// A valid IP address of the server. + /// The port the server is listening on. + public NetworkEndPoint(string IP, int port) : this(IPAddress.Parse(IP), port) + { + + } + } +} diff --git a/Hazel/NewConnectionEventArgs.cs b/Hazel/NewConnectionEventArgs.cs new file mode 100644 index 0000000..e4b6ab8 --- /dev/null +++ b/Hazel/NewConnectionEventArgs.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Hazel +{ + /// + /// Event args for new connection events. + /// + public class NewConnectionEventArgs : EventArgs + { + /// + /// The new connection. + /// + public Connection Connection { get; private set; } + + internal NewConnectionEventArgs(Connection Connection) + { + this.Connection = Connection; + } + } +} diff --git a/Hazel/Properties/AssemblyInfo.cs b/Hazel/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cfed1cb --- /dev/null +++ b/Hazel/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Hazel")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Hazel")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f3935f38-a904-40c7-ab9b-8d01aefe0489")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Hazel/SendFlags.cs b/Hazel/SendFlags.cs new file mode 100644 index 0000000..27251ff --- /dev/null +++ b/Hazel/SendFlags.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel +{ + /// + /// Specifies how a message should be sent. + /// + [Flags] + public enum SendOption : byte + { + /// + /// Requests unreliable delivery with no framentation or ordering. + /// + None = 0, + + /// + /// Requests data be sent reliably. Data is guaranteed to arrive at it's destination. + /// + Reliable = 1, + + /// + /// Requests that data should be sent in order. + /// + /// + /// Any packets that are out of order in this option will be dropped. + /// + Ordered = 2, + + /// + /// Requests that data should be sent in order and reliably. + /// + /// + /// Only messages that are sent using OrderedReliable or OrderedFragmentedReliable will arrive + /// in order, other messages + /// may arrive in between. + /// + OrderedReliable = 3, + + /// + /// Requests data be sent so that large messages are fragmented into smaller chunks of + /// data and reassembled when received. + /// + FragmentedReliable = 5, + + /// + /// Requests data be sent so that large messages are fragmented into smaller chunks of data and + /// reassembled when received and that the message arrives in order with other messages. + /// + OrderedFragmentedReliable = 7 + } +} diff --git a/Hazel/StateObject.cs b/Hazel/StateObject.cs new file mode 100644 index 0000000..f1fc1c3 --- /dev/null +++ b/Hazel/StateObject.cs @@ -0,0 +1,40 @@ +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 and Pipe connections. + /// + public 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/TcpConnection.cs b/Hazel/TcpConnection.cs new file mode 100644 index 0000000..5843491 --- /dev/null +++ b/Hazel/TcpConnection.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + /// + /// Represents a connection that uses the TCP protocol. + /// + public class TcpConnection : Connection + { + /// + /// The socket we're managing. + /// + public Socket Socket { get; private set; } + + /// + /// The remote end point of this connection. + /// + public EndPoint RemoteEndPoint { get; protected set; } + + /// + /// Creates a TcpConnection from a given TCP Socket. + /// + /// + 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."); + + this.EndPoint = new NetworkEndPoint(socket.RemoteEndPoint); + this.RemoteEndPoint = socket.RemoteEndPoint; + + this.Socket = socket; + + lock (this.Socket) + { + this.Socket.NoDelay = true; + } + + State = ConnectionState.Connected; + } + + /// + /// Creates a new TCP connection. + /// + public TcpConnection() + { + //Create and connect a socket + Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + + Socket.NoDelay = true; + } + + /// + /// Connects this TCP connection to the endpoint. + /// + /// The location of the server 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."); + } + + this.EndPoint = remoteEndPoint; + this.RemoteEndPoint = nep.EndPoint; + + //Connect + lock (Socket) + { + if (State != ConnectionState.NotConnected) + throw new InvalidOperationException("Cannot connect as the Connection is already connected."); + + 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 + try + { + StartWaitingForHeader(); + } + catch (SocketException e) + { + throw new HazelException("A Socket exception occured while initiating a receive operation.", e); + } + + //Set connected + lock (Socket) + State = ConnectionState.Connected; + } + + /// + /// Writes an array of bytes to the connection and prefixes the length. + /// + /// The bytes of the message to send. + /// The options this data is requested to send with. + /// + /// The sendOptions parameter is ignored by the TcpConnection as TCP only supports OrderedFragmentedReliable communication. + /// + public override void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.OrderedFragmentedReliable) + { + //Get bytes for length + byte[] fullBytes = Utility.AppendLengthHeader(bytes); + + //Write the bytes to the socket + lock (Socket) + { + 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 result of the async operation. + protected virtual void HeaderReadCallback(byte[] bytes) + { + //Get length + int length = Utility.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. + /// + /// + protected virtual void BodyReadCallback(byte[] bytes) + { + //Begin receiving from the start + StartWaitingForHeader(); + + Statistics.LogReceive(bytes.Length, bytes.Length + 4); + + //Fire DataReceived event + InvokeDataReceived(new DataEventArgs(bytes)); + } + + /// + /// Starts this connections waiting for the header. + /// + protected void StartWaitingForHeader() + { + StartWaitingForBytes(4, HeaderReadCallback); + } + + /// + /// Waits for the specified amount of bytes to be received. + /// + /// The number of bytes to receive. + /// The callback + protected virtual 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. + protected virtual void StartWaitingForChunk(StateObject state) + { + lock (Socket) + Socket.BeginReceive(state.buffer, state.totalBytesReceived, state.buffer.Length, SocketFlags.None, ChunkReadCallback, state); + } + + /// + /// Called when a chunk has been read. + /// + /// + protected virtual void ChunkReadCallback(IAsyncResult result) + { + int bytesReceived; + + //End the receive operation + try + { + lock (Socket) + bytesReceived = Socket.EndReceive(result); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + 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 (Socket) + { + //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(new DisconnectedEventArgs(e)); + + Dispose(); + } + } + + /// + /// Closes this connections safely. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (Socket) + { + 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 new file mode 100644 index 0000000..1c01ad6 --- /dev/null +++ b/Hazel/TcpConnectionListener.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + /// + /// Listens for new TCP connections and creates TCPConnections for them. + /// + public class TcpConnectionListener : ConnectionListener + { + /// + /// The IP address we're listening on. + /// + public IPAddress IPAddress { get; private set; } + + /// + /// The port we're listening on. + /// + public int Port { get; private set; } + + /// + /// The socket listening for connections. + /// + public Socket Listener { get; private set; } + + /// + /// Creates a new ConnectionListener for the given IP and port. + /// + /// The IPAddress to listen on. + /// The port to listen on. + public TcpConnectionListener(IPAddress IPAddress, int port) + { + this.IPAddress = IPAddress; + this.Port = port; + + this.Listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + } + + /// + /// Makes this connection listener begin listening for connections. + /// + 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); + + NewConnectionEventArgs args = new NewConnectionEventArgs(tcpConnection); + + FireNewConnectionEvent(args); + } + } + + /// + /// Called when the object is being disposed. + /// + /// Are we being disposed? + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (Listener) + Listener.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/UdpClientConnection.cs b/Hazel/UdpClientConnection.cs new file mode 100644 index 0000000..f9fc59b --- /dev/null +++ b/Hazel/UdpClientConnection.cs @@ -0,0 +1,232 @@ +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 +{ + public class UdpClientConnection : UdpConnection + { + /// + /// The socket we're connected via. + /// + Socket socket; + + /// + /// The buffer to store incomming data in. + /// + byte[] dataBuffer = new byte[ushort.MaxValue]; + + /// + /// Creates a new UdpClientConnection. + /// + public UdpClientConnection() + { + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + } + + /// + /// Writes an array of bytes to the connection. + /// + /// The bytes of the message to send. + /// The option this data is requested to send with. + public override void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None) + { + //Add sendflag byte to start + byte[] fullBytes = new byte[bytes.Length + 1]; + fullBytes[0] = (byte)sendOption; + Buffer.BlockCopy(bytes, 0, fullBytes, 1, bytes.Length); + + //Pack + SocketAsyncEventArgs args = new SocketAsyncEventArgs(); + args.SetBuffer(fullBytes, 0, fullBytes.Length); + args.RemoteEndPoint = RemoteEndPoint; + + lock (socket) + { + if (State != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. 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; + } + } + + Statistics.LogSend(bytes.Length, fullBytes.Length); + } + + /// + /// Connects this Connection to a given remote server and begins listening for data. + /// + 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."); + } + + this.EndPoint = nep; + this.RemoteEndPoint = nep.EndPoint; + + lock (socket) + { + if (State != ConnectionState.NotConnected) + throw new InvalidOperationException("Cannot connect as the Connection is already connected."); + + State = ConnectionState.Connecting; + + //Begin listening + try + { + //TODO should that really be IPAddress.Any? + 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) + { + throw new HazelException("Could not begin read as the socket has been disposed of, did you disconnect?"); + } + catch (SocketException e) + { + throw new HazelException("A Socket exception occured while initiating a receive operation.", e); + } + + State = ConnectionState.Connected; + } + + //Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present). + WriteBytes(new byte[] { 0 }, SendOption.Reliable); + } + + /// + /// Instructs the listener to begin listening. + /// + void StartListeningForData() + { + 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 (socket) + 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; + } + + //Copy to new buffer + byte[] buffer = new byte[bytesReceived]; + Buffer.BlockCopy((byte[])result.AsyncState, 1, buffer, 0, bytesReceived - 1); + + //Begin receiving again + try + { + lock (socket) + StartListeningForData(); + } + catch (SocketException e) + { + HandleDisconnect(new HazelException("A Socket exception occured while initiating a receive operation.", e)); + } + + Statistics.LogReceive(buffer.Length - 1, buffer.Length); + + InvokeDataReceived(new DataEventArgs(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 (socket) + { + //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(new DisconnectedEventArgs(e)); + + Dispose(); + } + } + + /// + /// Safely closes this connection. + /// + protected override void Dispose(bool disposing) + { + //Dispose of the socket + if (disposing) + { + lock (socket) + { + State = ConnectionState.NotConnected; + + socket.Dispose(); + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/UdpConnection.cs b/Hazel/UdpConnection.cs new file mode 100644 index 0000000..5c1b9ed --- /dev/null +++ b/Hazel/UdpConnection.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + /// + /// Represents a connection that uses the UDP protocol. + /// + public abstract class UdpConnection : Connection + { + /// + /// The packets of data that have been transmitted reliably and not acknowledged. + /// + Dictionary reliableDataPacketsSent = new Dictionary(); + + /// + /// Holds the last ID allocated. + /// + volatile uint lastIDAllocated; + + /// + /// The remote end point of this connection. + /// + public EndPoint RemoteEndPoint { get; protected set; } + + class Packet + { + public byte[] Data; + public DateTime SentTime; + + public Packet(byte[] data, DateTime sentTime) + { + Data = data; + SentTime = sentTime; + } + } + + /// + /// Handles the reliable/fragmented/ordered sending from this connection. + /// + /// The data being sent. + /// The send option. + /// The bytes that should actually be sent. + protected byte[] HandleSend(byte[] data, SendOption sendOption) + { + byte[] bytes = new byte[data.Length + 1]; + int offset = 1; + + if (sendOption == SendOption.Reliable) + { + bytes = new byte[data.Length + 5]; + offset = 5; + + lock (reliableDataPacketsSent) + { + //Find an ID not used yet. + uint id; + + do + id = ++lastIDAllocated; + while (reliableDataPacketsSent.ContainsKey(id)); + + bytes[1] = (byte)(id & 0xFF); + bytes[2] = (byte)((id >> 16) & 0xFF); + bytes[3] = (byte)((id >> 8) & 0xFF); + bytes[4] = (byte)id; + + //Remember packet + reliableDataPacketsSent.Add(id, new Packet(data, DateTime.Now)); + } + } + + Buffer.BlockCopy(data, 0, bytes, offset, bytes.Length); + + return bytes; + } + } +} diff --git a/Hazel/UdpConnectionListener.cs b/Hazel/UdpConnectionListener.cs new file mode 100644 index 0000000..a0151f2 --- /dev/null +++ b/Hazel/UdpConnectionListener.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +//TODO complete trawl through for thread safety, everywhere +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + /// + /// Listens for new UDP connections and creates UdpConnection for them. + /// + public class UdpConnectionListener : ConnectionListener + { + /// + /// The IP address we're listening on. + /// + public IPAddress IPAddress { get; private set; } + + /// + /// The port we're listening on. + /// + public int Port { get; private set; } + + /// + /// The socket listening for connections. + /// + Socket listener; + + /// + /// The connections we currently hold + /// + Dictionary connections = new Dictionary(); + + /// + /// Creates a new ConnectionListener for the given IP and port. + /// + /// The IPAddress to listen on. + /// The port to listen on. + public UdpConnectionListener(IPAddress IPAddress, int port) + { + this.IPAddress = IPAddress; + this.Port = port; + + this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + } + + /// + /// Instruct the listener to begin listening for connections. + /// + 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); + byte[] dataBuffer = new byte[ushort.MaxValue]; + + 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) //TODO how does this stop when the client disconnects? + bytesReceived = listener.EndReceiveFrom(result, ref remoteEndPoint); + } + catch (ObjectDisposedException) + { + //If the socket's been disposed then we can just end there. + return; + } + catch (SocketException e) + { + //TODO Errr...; + return; + } + + //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(); + + //If we're aware of this connection pass the data to the neccesary UdpConnection + bool exists; + lock (connections) + exists = connections.ContainsKey(remoteEndPoint); + + if (exists) + { + lock (connections) + connections[remoteEndPoint].InvokeDataReceived(buffer); + } + //If this is a new client then connect with them! + else + { + UdpServerConnection newConnection = new UdpServerConnection(this, remoteEndPoint); + lock (connections) + connections.Add(remoteEndPoint, newConnection); + + //And tell everyone about it! + FireNewConnectionEvent(new NewConnectionEventArgs(newConnection)); + } + } + + /// + /// 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); + } + } + + /// + /// Removes a virtual connection from the list. + /// + /// The endpoint of the virtual connection. + internal void RemoveConnectionTo(EndPoint endPoint) + { + lock (connections) + connections.Remove(endPoint); + } + + /// + /// Called when the listener is being disposed of + /// + /// + 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 new file mode 100644 index 0000000..ebf255f --- /dev/null +++ b/Hazel/UdpServerConnection.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +/* +* Copyright (C) Jamie Read - All Rights Reserved +* Unauthorized copying of this file, via any medium is strictly prohibited +* Proprietary and confidential +* Written by Jamie Read , January 2016 +*/ + +namespace Hazel +{ + class UdpServerConnection : UdpConnection + { + /// + /// The connection listener that we use the socket of. + /// + 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. + /// + /// + internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint) + { + this.Listener = listener; + this.RemoteEndPoint = endPoint; + this.EndPoint = new NetworkEndPoint(endPoint); + + State = ConnectionState.Connected; + } + + /// + /// Writes an array of bytes to the connection. + /// + /// The bytes of the message to send. + /// The option this data is requested to send with. + public override void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None) + { + //Add sendflag byte to start + byte[] fullBytes = new byte[bytes.Length + 1]; + fullBytes[0] = (byte)sendOption; + Buffer.BlockCopy(bytes, 0, fullBytes, 1, bytes.Length); + + lock (stateLock) + { + if (State != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + + Listener.SendData(fullBytes, RemoteEndPoint); + } + + Statistics.LogSend(bytes.Length, fullBytes.Length); + } + + /// + /// Connects this Connection to a given remote server. + /// + /// + /// 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 = new byte[buffer.Length - 1]; + Buffer.BlockCopy(buffer, 1, data, 0, data.Length); + + Statistics.LogReceive(data.Length, buffer.Length); + + InvokeDataReceived(new DataEventArgs(data)); + } + + /// + /// Safely closes this connection. + /// + protected override void Dispose(bool disposing) + { + //Here we just need to inform the listener we no longer need data. + if (disposing) + { + lock (stateLock) + { + Listener.RemoveConnectionTo(RemoteEndPoint); + + State = ConnectionState.NotConnected; + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Hazel/Utility.cs b/Hazel/Utility.cs new file mode 100644 index 0000000..98357cc --- /dev/null +++ b/Hazel/Utility.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hazel +{ + class Utility + { + /// + /// Appends the length header to the bytes. + /// + /// The source bytes. + /// + internal 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. + /// + /// + /// + internal 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]; + } + } +}