--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{1394E4CA-E17A-42F5-9216-8046ACA8D16B}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Hazel.UnitTests</RootNamespace>
+ <AssemblyName>Hazel.UnitTests</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
+ <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+ <ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
+ <IsCodedUITest>False</IsCodedUITest>
+ <TestProjectType>UnitTest</TestProjectType>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ </ItemGroup>
+ <Choose>
+ <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
+ <ItemGroup>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ </ItemGroup>
+ </When>
+ <Otherwise>
+ <ItemGroup>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
+ </ItemGroup>
+ </Otherwise>
+ </Choose>
+ <ItemGroup>
+ <Compile Include="TestHelper.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="TcpConnectionTests.cs" />
+ <Compile Include="UdpConnectionTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\Hazel\Hazel.csproj">
+ <Project>{02cfbd30-d77d-400f-94b2-700f60efdd7f}</Project>
+ <Name>Hazel</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Choose>
+ <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
+ <ItemGroup>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <Private>False</Private>
+ </Reference>
+ </ItemGroup>
+ </When>
+ </Choose>
+ <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
\ No newline at end of file
--- /dev/null
+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")]
--- /dev/null
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Net;
+
+namespace Hazel.UnitTests
+{
+ [TestClass]
+ public class TcpConnectionTests
+ {
+ /// <summary>
+ /// Tests the fields on TcpConnection.
+ /// </summary>
+ [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);
+ }
+ }
+
+ /// <summary>
+ /// Tests sending and receiving on the TcpConnection.
+ /// </summary>
+ [TestMethod]
+ public void TcpConnectionSendReceiveTest()
+ {
+ using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296))
+ using (TcpConnection connection = new TcpConnection())
+ {
+ TestHelper.RunSendReceiveTest(listener, connection, 4, 0, 0);
+ }
+ }
+ }
+}
--- /dev/null
+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
+ {
+ /// <summary>
+ /// Runs a general test on the given listener and connection.
+ /// </summary>
+ /// <param name="listener">The listener to test.</param>
+ /// <param name="connection">The connection to test.</param>
+ 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);
+ }
+ }
+}
--- /dev/null
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Net;
+
+namespace Hazel.UnitTests
+{
+ [TestClass]
+ public class UdpConnectionTests
+ {
+ /// <summary>
+ /// Tests the fields on UdpConnection.
+ /// </summary>
+ [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);
+ }
+ }
+
+ /// <summary>
+ /// Tests sending and receiving on the UdpConnection.
+ /// </summary>
+ [TestMethod]
+ public void UdpConnectionSendReceiveTest()
+ {
+ using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
+ using (UdpConnection connection = new UdpClientConnection())
+ {
+ TestHelper.RunSendReceiveTest(listener, connection, 1, 1, 2);
+ }
+ }
+ }
+}
--- /dev/null
+
+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
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ /// <summary>
+ /// Handles the sending and receiving of messages through the channel to give connection orientated, packet based transmission.
+ /// </summary>
+ public abstract class Connection : IDisposable
+ {
+ /// <summary>
+ /// Called when a message has been received.
+ /// </summary>
+ public event EventHandler<DataEventArgs> DataReceived;
+
+ /// <summary>
+ /// Called when the end point disconnects from us or an error occurs.
+ /// </summary>
+ public event EventHandler<DisconnectedEventArgs> Disconnected;
+
+ /// <summary>
+ /// The end point of this Connection.
+ /// </summary>
+ public ConnectionEndPoint EndPoint { get; protected set; }
+
+ /// <summary>
+ /// The traffic statistics about this Connection.
+ /// </summary>
+ public ConnectionStatistics Statistics { get; protected set; }
+
+ /// <summary>
+ /// The state of this connection.
+ /// </summary>
+ public ConnectionState State { get { return state; } protected set { state = value; } }
+ volatile ConnectionState state;
+
+ /// <summary>
+ /// Constructor that initializes the ConnecitonStatistics object.
+ /// </summary>
+ protected Connection()
+ {
+ Statistics = new ConnectionStatistics();
+
+ State = ConnectionState.NotConnected;
+ }
+
+ /// <summary>
+ /// Writes an array of bytes to the connection and prefixes the length.
+ /// </summary>
+ /// <param name="bytes">The bytes of the message to send.</param>
+ /// <param name="sendOption">The options this data is requested to send with.</param>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ public abstract void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None);
+
+ /// <summary>
+ /// Connects the connection to a remote server and begins listening.
+ /// </summary>
+ public abstract void Connect(ConnectionEndPoint remoteEndPoint);
+
+ /// <summary>
+ /// Invokes the DataReceived event to alert subscribers we received data.
+ /// </summary>
+ /// <param name="args">The arguments to supply.</param>
+ protected void InvokeDataReceived(DataEventArgs args)
+ {
+ //Make a copy to avoid race condition between null check and invocation
+ EventHandler<DataEventArgs> handler = DataReceived;
+ if (handler != null)
+ handler(this, args);
+ }
+
+ /// <summary>
+ /// Invokes the Disconnected event to alert hooked up methods there was an error or the remote end point disconnected.
+ /// </summary>
+ /// <param name="args">The arguments to supply.</param>
+ protected void InvokeDisconnected(DisconnectedEventArgs args)
+ {
+ //Make a copy to avoid race condition between null check and invocation
+ EventHandler<DisconnectedEventArgs> handler = Disconnected;
+ if (handler != null)
+ handler(this, args);
+ }
+
+ /// <summary>
+ /// Closes this connections safely.
+ /// </summary>
+ public void Close()
+ {
+ Dispose();
+ }
+
+ /// <summary>
+ /// Disposes of this NetworkConnection.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Disposes of this NetworkConnection.
+ /// </summary>
+ /// <param name="disposing">Are we currently disposing?</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Hazel
+{
+ public abstract class ConnectionEndPoint
+ {
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Base class for all connection listeners.
+ /// </summary>
+ public abstract class ConnectionListener : IDisposable
+ {
+ /// <summary>
+ /// Invoked when a new TCP connection is heard.
+ /// </summary>
+ public event EventHandler<NewConnectionEventArgs> NewConnection;
+
+ /// <summary>
+ /// Makes this connection listener begin listening for connections.
+ /// </summary>
+ public abstract void Start();
+
+ /// <summary>
+ /// Invokes the NewConnection event with the supplied args.
+ /// </summary>
+ /// <param name="args">The arguments for the event.</param>
+ protected void FireNewConnectionEvent(NewConnectionEventArgs args)
+ {
+ //Make a copy to avoid race condition between null check and invocation
+ EventHandler<NewConnectionEventArgs> handler = NewConnection;
+ if (handler != null)
+ handler(this, args);
+ }
+
+ /// <summary>
+ /// Call to dispose of the connection listener.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ /// <summary>
+ /// Called when the object is being disposed.
+ /// </summary>
+ /// <param name="disposing">Are we disposing?</param>
+ protected virtual void Dispose(bool disposing)
+ {
+
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Marks the state a Connection is currently in.
+ /// </summary>
+ public enum ConnectionState
+ {
+ /// <summary>
+ /// The Connection has either not been established yet or has been disconnected.
+ /// </summary>
+ NotConnected,
+
+ /// <summary>
+ /// The Connection is currently connecting to an endpoint.
+ /// </summary>
+ Connecting,
+
+ /// <summary>
+ /// The Connection is connected and data can be transfered.
+ /// </summary>
+ Connected,
+
+ /// <summary>
+ /// The Connection is currently disconnecting.
+ /// </summary>
+ Disconnecting
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Holds statistics about the traffic through a Connection.
+ /// </summary>
+ public class ConnectionStatistics
+ {
+ /// <summary>
+ /// The number of messages sent.
+ /// </summary>
+ public long MessagesSent
+ {
+ get
+ {
+ return Interlocked.Read(ref messagesSent);
+ }
+ }
+
+ /// <summary>
+ /// The number of messages sent.
+ /// </summary>
+ long messagesSent;
+
+ /// <summary>
+ /// The number of bytes of data sent.
+ /// </summary>
+ public long DataBytesSent
+ {
+ get
+ {
+ return Interlocked.Read(ref dataBytesSent);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes of data sent.
+ /// </summary>
+ long dataBytesSent;
+
+ /// <summary>
+ /// The number of bytes sent in total.
+ /// </summary>
+ public long TotalBytesSent
+ {
+ get
+ {
+ return Interlocked.Read(ref totalBytesSent);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes sent in total.
+ /// </summary>
+ long totalBytesSent;
+
+ /// <summary>
+ /// The number of messages received.
+ /// </summary>
+ public long MessagesReceived
+ {
+ get
+ {
+ return Interlocked.Read(ref messagesReceived);
+ }
+ }
+
+ /// <summary>
+ /// The number of messages received.
+ /// </summary>
+ long messagesReceived;
+
+ /// <summary>
+ /// The number of bytes of data received.
+ /// </summary>
+ public long DataBytesReceived
+ {
+ get
+ {
+ return Interlocked.Read(ref dataBytesReceived);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes of data received.
+ /// </summary>
+ long dataBytesReceived;
+
+ /// <summary>
+ /// The number of bytes received in total.
+ /// </summary>
+ public long TotalBytesReceived
+ {
+ get
+ {
+ return Interlocked.Read(ref totalBytesReceived);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes received in total.
+ /// </summary>
+ long totalBytesReceived;
+
+ /// <summary>
+ /// Logs the sending of a data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data sent.</param>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ internal void LogSend(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref messagesSent);
+ Interlocked.Add(ref dataBytesSent, dataLength);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of a data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data received.</param>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ internal void LogReceive(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref messagesReceived);
+ Interlocked.Add(ref dataBytesReceived, dataLength);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ public class DataEventArgs : EventArgs
+ {
+ /// <summary>
+ /// The bytes received.
+ /// </summary>
+ public byte[] Bytes;
+
+ /// <summary>
+ /// Creates DataEventArgs from bytes received.
+ /// </summary>
+ /// <param name="bytes"></param>
+ public DataEventArgs(byte[] bytes)
+ {
+ this.Bytes = bytes;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Events args for disconnected events.
+ /// </summary>
+ public class DisconnectedEventArgs
+ {
+ /// <summary>
+ /// The exception, if any, that caused the disconnect, otherwise null.
+ /// </summary>
+ public Exception Exception { get; private set; }
+
+ /// <summary>
+ /// Creates a DisconnectedEventArgs from the given exception or null
+ /// </summary>
+ /// <param name="e">The exception if the cause.</param>
+ internal DisconnectedEventArgs(Exception e)
+ {
+ this.Exception = e;
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{02CFBD30-D77D-400F-94B2-700F60EFDD7F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Hazel</RootNamespace>
+ <AssemblyName>Hazel</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <SignAssembly>true</SignAssembly>
+ </PropertyGroup>
+ <PropertyGroup>
+ <AssemblyOriginatorKeyFile>Hazel.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Connection.cs" />
+ <Compile Include="ConnectionEndPoint.cs" />
+ <Compile Include="ConnectionListener.cs" />
+ <Compile Include="ConnectionState.cs" />
+ <Compile Include="DataEventArgs.cs" />
+ <Compile Include="DisconnectedEventArgs.cs" />
+ <Compile Include="HazelException.cs" />
+ <Compile Include="NetworkEndPoint.cs" />
+ <Compile Include="NewConnectionEventArgs.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="SendFlags.cs" />
+ <Compile Include="StateObject.cs" />
+ <Compile Include="ConnectionStatistics.cs" />
+ <Compile Include="TcpConnection.cs" />
+ <Compile Include="TcpConnectionListener.cs" />
+ <Compile Include="UdpClientConnection.cs" />
+ <Compile Include="UdpConnection.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="UdpConnectionListener.cs" />
+ <Compile Include="UdpServerConnection.cs" />
+ <Compile Include="Utility.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Hazel.snk" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Wrapper for exceptions thrown from Hazel.
+ /// </summary>
+ class HazelException : Exception
+ {
+ internal HazelException(string msg) : base (msg)
+ {
+
+ }
+
+ internal HazelException(string msg, System.Net.Sockets.SocketException e) : base (msg, e)
+ {
+
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using System.Net;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Represents an endpoint to a remote resource on a network.
+ /// </summary>
+ public class NetworkEndPoint : ConnectionEndPoint
+ {
+ /// <summary>
+ /// The EndPoint this points to.
+ /// </summary>
+ public EndPoint EndPoint { get; set; }
+
+ /// <summary>
+ /// Creates a NetworkEndPoint from a given EndPoint.
+ /// </summary>
+ /// <param name="endPoint">The endpoint we represent./param>
+ public NetworkEndPoint(EndPoint endPoint)
+ {
+ this.EndPoint = endPoint;
+ }
+
+ /// <summary>
+ /// Create a NetworkEndPoint to the specified address and port.
+ /// </summary>
+ /// <param name="address">The IP address of the server.</param>
+ /// <param name="port">The port the server is listening on.</param>
+ public NetworkEndPoint(IPAddress address, int port) : this(new IPEndPoint(address, port))
+ {
+
+ }
+
+ /// <summary>
+ /// Creates a NetworkEndPoint to the specified IP address and port.
+ /// </summary>
+ /// <param name="IP">A valid IP address of the server.</param>
+ /// <param name="port">The port the server is listening on.</param>
+ public NetworkEndPoint(string IP, int port) : this(IPAddress.Parse(IP), port)
+ {
+
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Event args for new connection events.
+ /// </summary>
+ public class NewConnectionEventArgs : EventArgs
+ {
+ /// <summary>
+ /// The new connection.
+ /// </summary>
+ public Connection Connection { get; private set; }
+
+ internal NewConnectionEventArgs(Connection Connection)
+ {
+ this.Connection = Connection;
+ }
+ }
+}
--- /dev/null
+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")]
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Specifies how a message should be sent.
+ /// </summary>
+ [Flags]
+ public enum SendOption : byte
+ {
+ /// <summary>
+ /// Requests unreliable delivery with no framentation or ordering.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// Requests data be sent reliably. Data is guaranteed to arrive at it's destination.
+ /// </summary>
+ Reliable = 1,
+
+ /// <summary>
+ /// Requests that data should be sent in order.
+ /// </summary>
+ /// <remarks>
+ /// Any packets that are out of order in this option will be dropped.
+ /// </remarks>
+ Ordered = 2,
+
+ /// <summary>
+ /// Requests that data should be sent in order and reliably.
+ /// </summary>
+ /// <remarks>
+ /// Only messages that are sent using OrderedReliable or OrderedFragmentedReliable will arrive
+ /// in order, other messages
+ /// may arrive in between.
+ /// </remarks>
+ OrderedReliable = 3,
+
+ /// <summary>
+ /// Requests data be sent so that large messages are fragmented into smaller chunks of
+ /// data and reassembled when received.
+ /// </summary>
+ FragmentedReliable = 5,
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ OrderedFragmentedReliable = 7
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ /// <summary>
+ /// Represents the state of the current receive operation for TCP and Pipe connections.
+ /// </summary>
+ public struct StateObject
+ {
+ /// <summary>
+ /// The buffer we're receiving.
+ /// </summary>
+ internal byte[] buffer;
+
+ /// <summary>
+ /// The total number of bytes received so far.
+ /// </summary>
+ internal int totalBytesReceived;
+
+ /// <summary>
+ /// The callback to invoke once the buffer has been filled.
+ /// </summary>
+ internal Action<byte[]> callback;
+
+ /// <summary>
+ /// Creates a StateObject with the specified length.
+ /// </summary>
+ /// <param name="length">The number of bytes expected to be received.</param>
+ internal StateObject(int length, Action<byte[]> callback)
+ {
+ this.buffer = new byte[length];
+ this.totalBytesReceived = 0;
+ this.callback = callback;
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ /// <summary>
+ /// Represents a connection that uses the TCP protocol.
+ /// </summary>
+ public class TcpConnection : Connection
+ {
+ /// <summary>
+ /// The socket we're managing.
+ /// </summary>
+ public Socket Socket { get; private set; }
+
+ /// <summary>
+ /// The remote end point of this connection.
+ /// </summary>
+ public EndPoint RemoteEndPoint { get; protected set; }
+
+ /// <summary>
+ /// Creates a TcpConnection from a given TCP Socket.
+ /// </summary>
+ /// <param name="socket"></param>
+ 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;
+ }
+
+ /// <summary>
+ /// Creates a new TCP connection.
+ /// </summary>
+ public TcpConnection()
+ {
+ //Create and connect a socket
+ Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
+
+ Socket.NoDelay = true;
+ }
+
+ /// <summary>
+ /// Connects this TCP connection to the endpoint.
+ /// </summary>
+ /// <param name="remotEndPoint">The location of the server to connect to.</param>
+ 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;
+ }
+
+ /// <summary>
+ /// Writes an array of bytes to the connection and prefixes the length.
+ /// </summary>
+ /// <param name="bytes">The bytes of the message to send.</param>
+ /// <param name="sendOption">The options this data is requested to send with.</param>
+ /// <remarks>
+ /// The sendOptions parameter is ignored by the TcpConnection as TCP only supports OrderedFragmentedReliable communication.
+ /// </remarks>
+ 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);
+ }
+
+ /// <summary>
+ /// Called when a 4 byte header has been received.
+ /// </summary>
+ /// <param name="result">The result of the async operation.</param>
+ 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));
+ }
+ }
+
+ /// <summary>
+ /// Callback for when a body has been read.
+ /// </summary>
+ /// <param name="result"></param>
+ 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));
+ }
+
+ /// <summary>
+ /// Starts this connections waiting for the header.
+ /// </summary>
+ protected void StartWaitingForHeader()
+ {
+ StartWaitingForBytes(4, HeaderReadCallback);
+ }
+
+ /// <summary>
+ /// Waits for the specified amount of bytes to be received.
+ /// </summary>
+ /// <param name="length">The number of bytes to receive.</param>
+ /// <param name="callback">The callback </param>
+ protected virtual void StartWaitingForBytes(int length, Action<byte[]> callback)
+ {
+ StateObject state = new StateObject(length, callback);
+
+ StartWaitingForChunk(state);
+ }
+
+ /// <summary>
+ /// Waits for the next chunk of data from this socket.
+ /// </summary>
+ /// <param name="state">The StateObject for the receive operation.</param>
+ protected virtual void StartWaitingForChunk(StateObject state)
+ {
+ lock (Socket)
+ Socket.BeginReceive(state.buffer, state.totalBytesReceived, state.buffer.Length, SocketFlags.None, ChunkReadCallback, state);
+ }
+
+ /// <summary>
+ /// Called when a chunk has been read.
+ /// </summary>
+ /// <param name="result"></param>
+ 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);
+ }
+
+ /// <summary>
+ /// Called when the socket has been disconnected at the remote host.
+ /// </summary>
+ /// <param name="e">The exception if one was the cause.</param>
+ 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();
+ }
+ }
+
+ /// <summary>
+ /// Closes this connections safely.
+ /// </summary>
+ 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);
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ /// <summary>
+ /// Listens for new TCP connections and creates TCPConnections for them.
+ /// </summary>
+ public class TcpConnectionListener : ConnectionListener
+ {
+ /// <summary>
+ /// The IP address we're listening on.
+ /// </summary>
+ public IPAddress IPAddress { get; private set; }
+
+ /// <summary>
+ /// The port we're listening on.
+ /// </summary>
+ public int Port { get; private set; }
+
+ /// <summary>
+ /// The socket listening for connections.
+ /// </summary>
+ public Socket Listener { get; private set; }
+
+ /// <summary>
+ /// Creates a new ConnectionListener for the given IP and port.
+ /// </summary>
+ /// <param name="ipAdress">The IPAddress to listen on.</param>
+ /// <param name="port">The port to listen on.</param>
+ public TcpConnectionListener(IPAddress IPAddress, int port)
+ {
+ this.IPAddress = IPAddress;
+ this.Port = port;
+
+ this.Listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ }
+
+ /// <summary>
+ /// Makes this connection listener begin listening for connections.
+ /// </summary>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Called when a new connection has been accepted by the listener.
+ /// </summary>
+ /// <param name="result">The asyncronous operation's result.</param>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Called when the object is being disposed.
+ /// </summary>
+ /// <param name="disposing">Are we being disposed?</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ lock (Listener)
+ Listener.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
--- /dev/null
+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
+ {
+ /// <summary>
+ /// The socket we're connected via.
+ /// </summary>
+ Socket socket;
+
+ /// <summary>
+ /// The buffer to store incomming data in.
+ /// </summary>
+ byte[] dataBuffer = new byte[ushort.MaxValue];
+
+ /// <summary>
+ /// Creates a new UdpClientConnection.
+ /// </summary>
+ public UdpClientConnection()
+ {
+ socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ }
+
+ /// <summary>
+ /// Writes an array of bytes to the connection.
+ /// </summary>
+ /// <param name="bytes">The bytes of the message to send.</param>
+ /// <param name="sendOption">The option this data is requested to send with.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Connects this Connection to a given remote server and begins listening for data.
+ /// </summary>
+ 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);
+ }
+
+ /// <summary>
+ /// Instructs the listener to begin listening.
+ /// </summary>
+ void StartListeningForData()
+ {
+ socket.BeginReceive(dataBuffer, 0, dataBuffer.Length, SocketFlags.None, ReadCallback, dataBuffer);
+ }
+
+ /// <summary>
+ /// Called when data has been received by the socket.
+ /// </summary>
+ /// <param name="result">The asyncronous operation's result.</param>
+ 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));
+ }
+
+ /// <summary>
+ /// Called when the socket has been disconnected at the remote host.
+ /// </summary>
+ /// <param name="e">The exception if one was the cause.</param>
+ 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();
+ }
+ }
+
+ /// <summary>
+ /// Safely closes this connection.
+ /// </summary>
+ protected override void Dispose(bool disposing)
+ {
+ //Dispose of the socket
+ if (disposing)
+ {
+ lock (socket)
+ {
+ State = ConnectionState.NotConnected;
+
+ socket.Dispose();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ /// <summary>
+ /// Represents a connection that uses the UDP protocol.
+ /// </summary>
+ public abstract class UdpConnection : Connection
+ {
+ /// <summary>
+ /// The packets of data that have been transmitted reliably and not acknowledged.
+ /// </summary>
+ Dictionary<uint, Packet> reliableDataPacketsSent = new Dictionary<uint, Packet>();
+
+ /// <summary>
+ /// Holds the last ID allocated.
+ /// </summary>
+ volatile uint lastIDAllocated;
+
+ /// <summary>
+ /// The remote end point of this connection.
+ /// </summary>
+ public EndPoint RemoteEndPoint { get; protected set; }
+
+ class Packet
+ {
+ public byte[] Data;
+ public DateTime SentTime;
+
+ public Packet(byte[] data, DateTime sentTime)
+ {
+ Data = data;
+ SentTime = sentTime;
+ }
+ }
+
+ /// <summary>
+ /// Handles the reliable/fragmented/ordered sending from this connection.
+ /// </summary>
+ /// <param name="data">The data being sent.</param>
+ /// <param name="sendOption">The send option.</param>
+ /// <returns>The bytes that should actually be sent.</returns>
+ 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;
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ /// <summary>
+ /// Listens for new UDP connections and creates UdpConnection for them.
+ /// </summary>
+ public class UdpConnectionListener : ConnectionListener
+ {
+ /// <summary>
+ /// The IP address we're listening on.
+ /// </summary>
+ public IPAddress IPAddress { get; private set; }
+
+ /// <summary>
+ /// The port we're listening on.
+ /// </summary>
+ public int Port { get; private set; }
+
+ /// <summary>
+ /// The socket listening for connections.
+ /// </summary>
+ Socket listener;
+
+ /// <summary>
+ /// The connections we currently hold
+ /// </summary>
+ Dictionary<EndPoint, UdpServerConnection> connections = new Dictionary<EndPoint, UdpServerConnection>();
+
+ /// <summary>
+ /// Creates a new ConnectionListener for the given IP and port.
+ /// </summary>
+ /// <param name="ipAdress">The IPAddress to listen on.</param>
+ /// <param name="port">The port to listen on.</param>
+ public UdpConnectionListener(IPAddress IPAddress, int port)
+ {
+ this.IPAddress = IPAddress;
+ this.Port = port;
+
+ this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ }
+
+ /// <summary>
+ /// Instruct the listener to begin listening for connections.
+ /// </summary>
+ 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();
+ }
+
+ /// <summary>
+ /// Instructs the listener to begin listening.
+ /// </summary>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Called when data has been received by the listener.
+ /// </summary>
+ /// <param name="result">The asyncronous operation's result.</param>
+ 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));
+ }
+ }
+
+ /// <summary>
+ /// Sends data from the listener socket.
+ /// </summary>
+ /// <param name="bytes">The bytes to send.</param>
+ /// <param name="endPoint">The endpoint to send to.</param>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Removes a virtual connection from the list.
+ /// </summary>
+ /// <param name="endPoint">The endpoint of the virtual connection.</param>
+ internal void RemoveConnectionTo(EndPoint endPoint)
+ {
+ lock (connections)
+ connections.Remove(endPoint);
+ }
+
+ /// <summary>
+ /// Called when the listener is being disposed of
+ /// </summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ lock (listener)
+ listener.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
--- /dev/null
+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 <jamie.read@outlook.com>, January 2016
+*/
+
+namespace Hazel
+{
+ class UdpServerConnection : UdpConnection
+ {
+ /// <summary>
+ /// The connection listener that we use the socket of.
+ /// </summary>
+ public UdpConnectionListener Listener { get; private set; }
+
+ /// <summary>
+ /// Lock object for the writing to the state of the connection.
+ /// </summary>
+ Object stateLock = new Object();
+
+ /// <summary>
+ /// Creates a UdpConnection for the virtual connection to the endpoint.
+ /// </summary>
+ /// <param name="socket"></param>
+ internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint)
+ {
+ this.Listener = listener;
+ this.RemoteEndPoint = endPoint;
+ this.EndPoint = new NetworkEndPoint(endPoint);
+
+ State = ConnectionState.Connected;
+ }
+
+ /// <summary>
+ /// Writes an array of bytes to the connection.
+ /// </summary>
+ /// <param name="bytes">The bytes of the message to send.</param>
+ /// <param name="sendOption">The option this data is requested to send with.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Connects this Connection to a given remote server.
+ /// </summary>
+ /// <remarks>
+ /// This will always throw an InvalidOperationException.
+ /// </remarks>
+ public override void Connect(ConnectionEndPoint remoteEndPoint)
+ {
+ throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
+ }
+
+ /// <summary>
+ /// Called by the listener when we have data.
+ /// </summary>
+ /// <param name="buffer"></param>
+ 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));
+ }
+
+ /// <summary>
+ /// Safely closes this connection.
+ /// </summary>
+ 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);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ class Utility
+ {
+ /// <summary>
+ /// Appends the length header to the bytes.
+ /// </summary>
+ /// <param name="bytes">The source bytes.</param>
+ /// <returns></returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Returns the length from a length header.
+ /// </summary>
+ /// <param name="bytes"></param>
+ /// <returns></returns>
+ 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];
+ }
+ }
+}