diff --git a/Hazel.UnitTests/TcpConnectionTests.cs b/Hazel.UnitTests/TcpConnectionTests.cs index 9ef0772..92317d0 100644 --- a/Hazel.UnitTests/TcpConnectionTests.cs +++ b/Hazel.UnitTests/TcpConnectionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Threading; @@ -136,5 +137,66 @@ public void ServerDisconnectTest() TestHelper.RunServerDisconnectTest(listener, connection); } } + + /// + /// Tests the keepalive functionality from the client, + /// + [TestMethod] + public void KeepAliveClientTest() + { + using (TcpConnectionListener listener = new TcpConnectionListener(new NetworkEndPoint(IPAddress.Any, 4296))) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) + { + listener.Start(); + + connection.Connect(); + connection.KeepAliveInterval = 100; + + System.Threading.Thread.Sleep(1050); //Enough time for ~10 keep alive packets + + Assert.IsTrue( + connection.Statistics.TotalBytesSent >= 30 && + connection.Statistics.TotalBytesSent <= 50, + "Sent: " + connection.Statistics.TotalBytesSent + ); + } + } + + /// + /// Tests the keepalive functionality from the client, + /// + [TestMethod] + public void KeepAliveServerTest() + { + ManualResetEvent mutex = new ManualResetEvent(false); + TcpConnection listenerConnectionToClient = null; + + using (TcpConnectionListener listener = new TcpConnectionListener(new NetworkEndPoint(IPAddress.Any, 4296))) + using (TcpConnection connection = new TcpConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) + { + listener.NewConnection += delegate (object sender, NewConnectionEventArgs args) + { + listenerConnectionToClient = (TcpConnection)args.Connection; + listenerConnectionToClient.KeepAliveInterval = 100; + + Thread.Sleep(1050); //Enough time for ~10 keep alive packets + + mutex.Set(); + }; + + listener.Start(); + + connection.Connect(); + + mutex.WaitOne(); + + Assert.IsNotNull(listenerConnectionToClient); + Assert.IsTrue( + listenerConnectionToClient.Statistics.TotalBytesSent >= 30 && + listenerConnectionToClient.Statistics.TotalBytesSent <= 50, + "Sent: " + listenerConnectionToClient.Statistics.TotalBytesSent + ); + } + } } } diff --git a/Hazel.UnitTests/UdpConnectionTests.cs b/Hazel.UnitTests/UdpConnectionTests.cs index 7216fb4..ead4800 100644 --- a/Hazel.UnitTests/UdpConnectionTests.cs +++ b/Hazel.UnitTests/UdpConnectionTests.cs @@ -194,22 +194,18 @@ public void KeepAliveClientTest() public void KeepAliveServerTest() { ManualResetEvent mutex = new ManualResetEvent(false); + UdpConnection listenerConnectionToClient = null; using (UdpConnectionListener listener = new UdpConnectionListener(new NetworkEndPoint(IPAddress.Any, 4296))) using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296))) { listener.NewConnection += delegate(object sender, NewConnectionEventArgs args) { - ((UdpConnection)args.Connection).KeepAliveInterval = 100; + listenerConnectionToClient = (UdpConnection) args.Connection; + listenerConnectionToClient.KeepAliveInterval = 100; Thread.Sleep(1050); //Enough time for ~10 keep alive packets - Assert.IsTrue( - args.Connection.Statistics.TotalBytesSent >= 30 && - args.Connection.Statistics.TotalBytesSent <= 50, - "Sent: " + args.Connection.Statistics.TotalBytesSent - ); - mutex.Set(); }; @@ -218,6 +214,13 @@ public void KeepAliveServerTest() connection.Connect(); mutex.WaitOne(); + + Assert.IsNotNull(listenerConnectionToClient); + Assert.IsTrue( + listenerConnectionToClient.Statistics.TotalBytesSent >= 30 && + listenerConnectionToClient.Statistics.TotalBytesSent <= 50, + "Sent: " + listenerConnectionToClient.Statistics.TotalBytesSent + ); } } diff --git a/Hazel/Udp/UdpConnection.KeepAlive.cs b/Hazel/Connection.KeepAlive.cs similarity index 79% rename from Hazel/Udp/UdpConnection.KeepAlive.cs rename to Hazel/Connection.KeepAlive.cs index f1d6f3c..7126c51 100644 --- a/Hazel/Udp/UdpConnection.KeepAlive.cs +++ b/Hazel/Connection.KeepAlive.cs @@ -3,12 +3,13 @@ using System.Diagnostics; using System.Linq; using System.Text; +using System.Net.Sockets; +using System.Net; using System.Threading; - -namespace Hazel.Udp +namespace Hazel { - partial class UdpConnection + partial class Connection { /// /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds. @@ -33,7 +34,7 @@ public int KeepAliveInterval set { keepAliveInterval = value; - + //Update timer ResetKeepAliveTimer(); } @@ -55,10 +56,12 @@ public int KeepAliveInterval /// bool keepAliveTimerDisposed; + readonly byte[] keepAlivePacket = new byte[1]; + /// /// Starts the keepalive timer. /// - void InitializeKeepAliveTimer() + protected void InitializeKeepAliveTimer() { lock (keepAliveTimerLock) { @@ -66,7 +69,7 @@ void InitializeKeepAliveTimer() (o) => { Trace.WriteLine("Keepalive packet sent."); - SendHello(null, null); + SendBytes(keepAlivePacket, SendOption.KeepAlive); }, null, keepAliveInterval, @@ -78,20 +81,23 @@ void InitializeKeepAliveTimer() /// /// Resets the keepalive timer to zero. /// - void ResetKeepAliveTimer() + protected void ResetKeepAliveTimer() { lock (keepAliveTimerLock) - keepAliveTimer.Change(keepAliveInterval, keepAliveInterval); + { + if(keepAliveTimer != null) + keepAliveTimer.Change(keepAliveInterval, keepAliveInterval); + } } /// /// Disposes of the keep alive timer. /// - void DisposeKeepAliveTimer() + protected void DisposeKeepAliveTimer() { - lock(keepAliveTimerLock) + lock (keepAliveTimerLock) { - if (!keepAliveTimerDisposed) + if (!keepAliveTimerDisposed && keepAliveTimer != null) keepAliveTimer.Dispose(); keepAliveTimerDisposed = true; } diff --git a/Hazel/Connection.cs b/Hazel/Connection.cs index 7bc9d57..c2e8029 100644 --- a/Hazel/Connection.cs +++ b/Hazel/Connection.cs @@ -32,7 +32,7 @@ namespace Hazel /// /// /// - public abstract class Connection : IDisposable + public abstract partial class Connection : IDisposable { /// /// Called when a message has been received. @@ -261,6 +261,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { + DisposeKeepAliveTimer(); } } } diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index 5cf54e2..2b86c87 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -49,6 +49,7 @@ + @@ -75,7 +76,6 @@ Code - diff --git a/Hazel/SendOption.cs b/Hazel/SendOption.cs index e7ebec2..20d2287 100644 --- a/Hazel/SendOption.cs +++ b/Hazel/SendOption.cs @@ -42,6 +42,15 @@ public enum SendOption : byte /// guaranteed to arrive and to arrive only once but the sending process may require more memory, processing, /// a larger number protocol bytes and may be slower than sending unreliably. /// - FragmentedReliable = 2 + FragmentedReliable = 2, + + /// + /// Requests data be treated as keep alive packet. + /// + /// + /// This packet type is used soly to keep connection working. + /// Data inside keep alive packet will be ignored. + /// + KeepAlive = 3 } } diff --git a/Hazel/Tcp/TcpConnection.cs b/Hazel/Tcp/TcpConnection.cs index 99d60c1..148ec54 100644 --- a/Hazel/Tcp/TcpConnection.cs +++ b/Hazel/Tcp/TcpConnection.cs @@ -42,6 +42,8 @@ internal TcpConnection(Socket socket) this.socket.NoDelay = true; State = ConnectionState.Connected; + + InitializeKeepAliveTimer(); } } @@ -69,7 +71,7 @@ public TcpConnection(NetworkEndPoint remoteEndPoint) throw new HazelException("IPV6 not supported!"); socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - socket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, false); + socket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, false); } socket.NoDelay = true; @@ -123,6 +125,8 @@ public override void Connect(byte[] bytes = null, int timeout = 5000) State = ConnectionState.Connected; SendBytes(actualBytes); + + InitializeKeepAliveTimer(); } } @@ -130,14 +134,15 @@ public override void Connect(byte[] bytes = null, int timeout = 5000) /// /// /// - /// The sendOption parameter is ignored by the TcpConnection as TCP only supports FragmentedReliable - /// communication, specifying anything else will have no effect. + /// The sendOption parameter can only be set as FragmentedReliable or KeepAlive. + /// TCP only supports FragmentedReliable communication, specifying anything else will have no effect. + /// KeepAlive packets are still send as FragmentedReliable. /// /// public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption.FragmentedReliable) { //Get bytes for length - byte[] fullBytes = AppendLengthHeader(bytes); + byte[] fullBytes = AppendLengthHeader(bytes, sendOption); //Write the bytes to the socket lock (socketLock) @@ -145,6 +150,8 @@ public override void SendBytes(byte[] bytes, SendOption sendOption = SendOption. if (State != ConnectionState.Connected) throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + ResetKeepAliveTimer(); + try { socket.BeginSend(fullBytes, 0, fullBytes.Length, SocketFlags.None, null, null); @@ -170,6 +177,22 @@ void HeaderReadCallback(byte[] bytes, Action callback) //Get length int length = GetLengthFromBytes(bytes); + //Check if there is no body -> [KeepAlive] packet, if so then log it and wait for next message + if (length == 0) + { + try + { + StartWaitingForHeader(callback); + } + catch (Exception e) + { + HandleDisconnect(new HazelException("An exception occured while initiating a header receive operation.", e)); + } + + Statistics.LogHelloReceive(bytes.Length + 4); + return; + } + //Begin receiving the body try { @@ -303,6 +326,8 @@ void ChunkReadCallback(IAsyncResult result) return; } + ResetKeepAliveTimer(); + StateObject state = (StateObject)result.AsyncState; state.totalBytesReceived += bytesReceived; //TODO threading issues on state? @@ -362,16 +387,28 @@ void HandleDisconnect(HazelException e = null) /// Appends the length header to the bytes. /// /// The source bytes. + /// Allows for marking packet as KeepAlive. (Zero body length) /// The new bytes. - static byte[] AppendLengthHeader(byte[] bytes) + static byte[] AppendLengthHeader(byte[] bytes, SendOption sendOption) { 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; + if (sendOption == SendOption.KeepAlive) + { + //Append length as 0 + fullBytes[0] = 0; + fullBytes[1] = 0; + fullBytes[2] = 0; + fullBytes[3] = 0; + } + else + { + //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); diff --git a/Hazel/Udp/UdpClientConnection.cs b/Hazel/Udp/UdpClientConnection.cs index 4ffe424..e74645a 100644 --- a/Hazel/Udp/UdpClientConnection.cs +++ b/Hazel/Udp/UdpClientConnection.cs @@ -160,6 +160,8 @@ public override void Connect(byte[] bytes = null, int timeout = 5000) Dispose(); throw new HazelException("Connection attempt timed out."); } + + InitializeKeepAliveTimer(); } /// diff --git a/Hazel/Udp/UdpConnection.Fragmented.cs b/Hazel/Udp/UdpConnection.Fragmented.cs index abea8c4..96f74de 100644 --- a/Hazel/Udp/UdpConnection.Fragmented.cs +++ b/Hazel/Udp/UdpConnection.Fragmented.cs @@ -10,7 +10,7 @@ partial class UdpConnection /// /// The amount of data that can be put into a fragment. /// - public int FragmentSize { get { return _fragmentSize; } } + public int FragmentSize { get { return fragmentSize; } } int fragmentSize = 65507 - 1 - 2 - 2 - 2; /// diff --git a/Hazel/Udp/UdpConnection.cs b/Hazel/Udp/UdpConnection.cs index 6f97d2d..97475d9 100644 --- a/Hazel/Udp/UdpConnection.cs +++ b/Hazel/Udp/UdpConnection.cs @@ -14,14 +14,6 @@ namespace Hazel.Udp /// public abstract partial class UdpConnection : NetworkConnection { - /// - /// Creates a new UdpConnection and initializes the keep alive timer. - /// - protected UdpConnection() - { - InitializeKeepAliveTimer(); - } - /// /// Writes the given bytes to the connection. /// @@ -63,6 +55,7 @@ protected void HandleSend(byte[] data, byte sendOption, Action ackCallback = nul { //Handle reliable header and hellos case (byte)SendOption.Reliable: + case (byte)SendOption.KeepAlive: case (byte)UdpSendOption.Hello: ReliableSend(sendOption, data, ackCallback); break; @@ -99,7 +92,8 @@ protected internal void HandleReceive(byte[] buffer) AcknowledgementMessageReceive(buffer); break; - //We need to acknowledge hello messages but dont want to invoke any events! + //We need to acknowledge hello/keep alive messages but dont want to invoke any events! + case (byte)SendOption.KeepAlive: case (byte)UdpSendOption.Hello: ProcessReliableReceive(buffer, 1); Statistics.LogHelloReceive(buffer.Length); @@ -195,16 +189,5 @@ protected void SendDisconnect() { HandleSend(new byte[0], (byte)UdpSendOption.Disconnect); //TODO Should disconnect wait for an ack? } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - DisposeKeepAliveTimer(); - } - - base.Dispose(disposing); - } } } diff --git a/Hazel/Udp/UdpServerConnection.cs b/Hazel/Udp/UdpServerConnection.cs index 566bf1e..415f706 100644 --- a/Hazel/Udp/UdpServerConnection.cs +++ b/Hazel/Udp/UdpServerConnection.cs @@ -42,6 +42,8 @@ internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint, this.IPMode = IPMode; State = ConnectionState.Connected; + + InitializeKeepAliveTimer(); } ///