using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296))
using (TcpConnection connection = new TcpConnection())
{
- TestHelper.RunServerToClientTest(listener, connection, 4, 0, 0, SendOption.OrderedFragmentedReliable);
+ TestHelper.RunServerToClientTest(listener, connection, 4, 0, SendOption.OrderedFragmentedReliable);
}
}
using (TcpConnectionListener listener = new TcpConnectionListener(IPAddress.Any, 4296))
using (TcpConnection connection = new TcpConnection())
{
- TestHelper.RunClientToServerTest(listener, connection, 4, 0, 0, SendOption.OrderedFragmentedReliable);
+ TestHelper.RunClientToServerTest(listener, connection, 4, 0, SendOption.OrderedFragmentedReliable);
}
}
}
/// </summary>
/// <param name="listener">The listener to test.</param>
/// <param name="connection">The connection to test.</param>
- internal static void RunServerToClientTest(ConnectionListener listener, Connection connection, int headerSize, int handshakeSize, int totalHandshakeSize, SendOption sendOption)
+ internal static void RunServerToClientTest(ConnectionListener listener, Connection connection, int headerSize, int totalHandshakeSize, SendOption sendOption)
{
//Setup meta stuff
byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//Setup listener
listener.NewConnection += delegate(object sender, NewConnectionEventArgs args)
{
+ Assert.AreEqual(0, args.Connection.Statistics.DataBytesReceived);
+ Assert.AreEqual(0, args.Connection.Statistics.TotalBytesReceived);
+
args.Connection.WriteBytes(data, sendOption);
+
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();
//Wait until data is received
mutex.WaitOne();
- Assert.AreEqual(handshakeSize, connection.Statistics.DataBytesSent);
+ Assert.AreEqual(0, connection.Statistics.DataBytesSent);
Assert.AreEqual(data.Length, connection.Statistics.DataBytesReceived);
Assert.AreEqual(totalHandshakeSize, connection.Statistics.TotalBytesSent);
Assert.AreEqual(data.Length + headerSize, connection.Statistics.TotalBytesReceived);
/// </summary>
/// <param name="listener">The listener to test.</param>
/// <param name="connection">The connection to test.</param>
- internal static void RunClientToServerTest(ConnectionListener listener, Connection connection, int headerSize, int handshakeSize, int totalHandshakeSize, SendOption sendOption)
+ internal static void RunClientToServerTest(ConnectionListener listener, Connection connection, int headerSize, int totalHandshakeSize, SendOption sendOption)
{
//Setup meta stuff
byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//Wait until data is received
mutex.WaitOne();
- Assert.AreEqual(data.Length + handshakeSize, connection.Statistics.DataBytesSent);
+ Assert.AreEqual(data.Length, connection.Statistics.DataBytesSent);
Assert.AreEqual(0, connection.Statistics.DataBytesReceived);
Assert.AreEqual(totalHandshakeSize + data.Length + headerSize, connection.Statistics.TotalBytesSent);
- Assert.AreEqual(sendOption == SendOption.Reliable ? 3 : 0, connection.Statistics.TotalBytesReceived);
+ Assert.AreEqual(0, connection.Statistics.TotalBytesReceived);
}
}
}
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net;
+using System.Threading;
namespace Hazel.UnitTests
{
//UdpConnection fields
Assert.AreEqual(new IPEndPoint(IPAddress.Loopback, 4296), connection.RemoteEndPoint);
- Assert.AreEqual(1, connection.Statistics.DataBytesSent);
+ Assert.AreEqual(0, connection.Statistics.DataBytesSent);
Assert.AreEqual(0, connection.Statistics.DataBytesReceived);
}
}
using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
using (UdpConnection connection = new UdpClientConnection())
{
- TestHelper.RunServerToClientTest(listener, connection, 1, 1, 2, SendOption.None);
+ TestHelper.RunServerToClientTest(listener, connection, 1, 3, SendOption.None);
}
}
using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
using (UdpConnection connection = new UdpClientConnection())
{
- TestHelper.RunServerToClientTest(listener, connection, 3, 1, 2, SendOption.Reliable);
+ TestHelper.RunServerToClientTest(listener, connection, 3, 3, SendOption.Reliable);
}
}
using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
using (UdpConnection connection = new UdpClientConnection())
{
- TestHelper.RunClientToServerTest(listener, connection, 1, 1, 2, SendOption.None);
+ TestHelper.RunClientToServerTest(listener, connection, 1, 3, SendOption.None);
}
}
using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
using (UdpConnection connection = new UdpClientConnection())
{
- TestHelper.RunClientToServerTest(listener, connection, 3, 1, 2, SendOption.Reliable);
+ TestHelper.RunClientToServerTest(listener, connection, 3, 3, SendOption.Reliable);
+ }
+ }
+
+ /// <summary>
+ /// Tests the keepalive functionality from the client,
+ /// </summary>
+ [TestMethod]
+ public void KeepAliveClientTest()
+ {
+ using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
+ using (UdpConnection connection = new UdpClientConnection())
+ {
+ listener.Start();
+
+ connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296));
+ connection.KeepAliveInterval = 100;
+
+ System.Threading.Thread.Sleep(1100); //Enough time for 10 keep alive packets
+
+ Assert.AreEqual(33, connection.Statistics.TotalBytesSent);
+ }
+ }
+
+ /// <summary>
+ /// Tests the keepalive functionality from the client,
+ /// </summary>
+ [TestMethod]
+ public void KeepAliveServerTest()
+ {
+ ManualResetEvent mutex = new ManualResetEvent(false);
+
+ using (UdpConnectionListener listener = new UdpConnectionListener(IPAddress.Any, 4296))
+ using (UdpConnection connection = new UdpClientConnection())
+ {
+ listener.NewConnection += delegate(object sender, NewConnectionEventArgs args)
+ {
+ ((UdpConnection)args.Connection).KeepAliveInterval = 100;
+
+ Thread.Sleep(1100); //Enough time for 10 keep alive packets
+
+ Assert.AreEqual(30, args.Connection.Statistics.TotalBytesSent);
+ mutex.Set();
+ };
+
+ listener.Start();
+
+ connection.Connect(new NetworkEndPoint(IPAddress.Loopback, 4296));
+
+ mutex.WaitOne();
}
}
}
using System.Text;
using System.Net.Sockets;
using System.Net;
+using System.Threading;
/*
/// <summary>
/// The state of this connection.
/// </summary>
- public ConnectionState State { get { return state; } protected set { state = value; } }
+ public ConnectionState State
+ {
+ get
+ {
+ return state;
+ }
+
+ protected set
+ {
+ state = value;
+
+ if (state == ConnectionState.Connected)
+ connectWaitLock.Set();
+ else
+ connectWaitLock.Reset();
+ }
+ }
volatile ConnectionState state;
+ /// <summary>
+ /// Reset event that is triggered when the connection is marked Connected.
+ /// </summary>
+ ManualResetEvent connectWaitLock = new ManualResetEvent(false);
+
/// <summary>
/// Constructor that initializes the ConnecitonStatistics object.
/// </summary>
handler(this, args);
}
+ /// <summary>
+ /// Blocks until the Connection is connected.
+ /// </summary>
+ protected void WaitOnConnect()
+ {
+ connectWaitLock.WaitOne();
+ }
+
/// <summary>
/// Closes this connections safely.
/// </summary>
<Compile Include="UdpConnection.cs">
<SubType>Code</SubType>
</Compile>
+ <Compile Include="UdpConnection.KeepAlive.cs" />
<Compile Include="UdpConnection.Reliable.cs" />
<Compile Include="UdpConnectionListener.cs" />
<Compile Include="UdpServerConnection.cs" />
/// </summary>
enum SendOptionInternal : byte
{
+ /// <summary>
+ /// Hello message for initiating communication.
+ /// </summary>
+ Hello = 254,
+
+ /// <summary>
+ /// Message acknowledging the receipt of a message.
+ /// </summary>
Acknowledgement = 255
}
}
Socket.NoDelay = true;
}
+ /// <summary>
+ /// Internal call to start listening once this socket has been constructed and is ready.
+ /// </summary>
+ internal void StartListening()
+ {
+ //Start receiving data
+ try
+ {
+ StartWaitingForHeader();
+ }
+ catch (SocketException e)
+ {
+ throw new HazelException("A Socket exception occured while initiating a receive operation.", e);
+ }
+ }
+
/// <summary>
/// Connects this TCP connection to the endpoint.
/// </summary>
NewConnectionEventArgs args = new NewConnectionEventArgs(tcpConnection);
FireNewConnectionEvent(args);
+
+ tcpConnection.StartListening();
}
}
/// Creates a new UdpClientConnection.
/// </summary>
public UdpClientConnection()
+ : base()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
}
/// <param name="sendOption">The option this data is requested to send with.</param>
public override void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None)
{
+ if (State != ConnectionState.Connected)
+ throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?");
+
//Add header information and send
- HandleSend(bytes, sendOption);
+ HandleSend(bytes, (byte)sendOption);
}
/// <summary>
lock (socket)
{
- if (State != ConnectionState.Connected)
- throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?");
+ if (State != ConnectionState.Connected && State != ConnectionState.Connecting)
+ throw new InvalidOperationException("Could not send data as this Connection is not connected and is not connecting. Did you disconnect?");
try
{
NetworkEndPoint nep = remoteEndPoint as NetworkEndPoint;
if (nep == null)
{
- throw new ArgumentException("The remote end point of a TCP connection must be a NetworkEndPoint.");
+ throw new ArgumentException("The remote end point of a UDP connection must be a NetworkEndPoint.");
}
this.EndPoint = nep;
{
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.None); //TODO special hello message
+ //Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present)
+ //When acknowledged set the state to connected
+ SendHello(() => State = ConnectionState.Connected);
+
+ //Wait till hello packet is acknowledged and the state is set to Connected
+ WaitOnConnect();
}
/// <summary>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+ /// <summary>
+ /// UdpConnection part which handles keepalive packets.
+ /// </summary>
+ partial class UdpConnection
+ {
+ /// <summary>
+ /// The interval from data being received or transmitted to a keepalive packet being sent.
+ /// </summary>
+ /// <remarks>
+ /// Set to System.Threading.Timeout.Infinite to disable keepalive packets.
+ /// </remarks>
+ public int KeepAliveInterval
+ {
+ get
+ {
+ return keepAliveInterval;
+ }
+
+ set
+ {
+ keepAliveInterval = value;
+
+ //Update timer
+ ResetKeepAliveTimer();
+ }
+ }
+ int keepAliveInterval = 10000;
+
+ /// <summary>
+ /// The timer creating keepalive pulses.
+ /// </summary>
+ Timer keepAliveTimer;
+
+ /// <summary>
+ /// Lock for keep alive timer.
+ /// </summary>
+ Object keepAliveTimerLock = new Object();
+
+ /// <summary>
+ /// Starts the keepalive timer.
+ /// </summary>
+ void InitializeKeepAliveTimer()
+ {
+ lock (keepAliveTimerLock)
+ {
+ keepAliveTimer = new Timer(
+ (o) =>
+ {
+ Trace.WriteLine("Keepalive packet sent.");
+ SendHello(null);
+ },
+ null,
+ keepAliveInterval,
+ keepAliveInterval
+ );
+ }
+ }
+
+ /// <summary>
+ /// Resets the keepalive timer to zero.
+ /// </summary>
+ void ResetKeepAliveTimer()
+ {
+ lock (keepAliveTimerLock)
+ keepAliveTimer.Change(keepAliveInterval, keepAliveInterval);
+ }
+
+ /// <summary>
+ /// Disposes of the keep alive timer.
+ /// </summary>
+ void DisposeKeepAliveTimer()
+ {
+ lock(keepAliveTimerLock)
+ keepAliveTimer.Dispose();
+ }
+ }
+}
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
public byte[] Data;
public Timer Timer;
public int LastTimeout;
+ public Action AckCallback;
- public Packet(byte[] data, Action<Packet> resendAction, int timeout)
+ public Packet(byte[] data, Action<Packet> resendAction, int timeout, Action ackCallback)
{
Data = data;
(object obj) => resendAction(this),
null,
timeout,
- timeout
+ Timeout.Infinite
);
LastTimeout = timeout;
+ AckCallback = ackCallback;
}
}
/// Writes the bytes neccessary for a reliable send and stores the send.
/// </summary>
/// <param name="bytes">The byte array to write to.</param>
- void WriteReliableSendHeader(byte[] bytes)
+ void WriteReliableSendHeader(byte[] bytes, Action ackCallback)
{
lock (reliableDataPacketsSent)
{
WriteBytesToConnection(p.Data);
//Double packet timeout
- p.Timer.Change(0, p.LastTimeout *= 2);
+ lock (p.Timer)
+ p.Timer.Change(p.LastTimeout *= 2, Timeout.Infinite);
+
+ Trace.WriteLine("Resend.");
},
- resendTimeout
+ resendTimeout,
+ ackCallback
);
//Remember packet
//Get the ID form the packet
ushort id = (ushort)((bytes[1] << 8) + bytes[2]);
- //Always reply with acknowledgement in order to stop the sender repeatedly sending it
- WriteBytesToConnection( //TODO group acks together
- new byte[]
- {
- (byte)SendOptionInternal.Acknowledgement,
- bytes[1],
- bytes[2]
- }
- );
+ //Send an acknowledgement
+ SendAck(bytes[1], bytes[2]);
//Handle reliableness!
lock (reliableDataPacketsMissing)
//Dispose of timer and remove from dictionary
if (reliableDataPacketsSent.ContainsKey(id))
{
- reliableDataPacketsSent[id].Timer.Dispose();
+ Packet packet = reliableDataPacketsSent[id];
+
+ lock (packet.Timer)
+ packet.Timer.Dispose();
+
+ if (packet.AckCallback != null)
+ packet.AckCallback.Invoke();
+
reliableDataPacketsSent.Remove(id);
}
}
}
+
+ internal void SendAck(byte byte1, byte byte2)
+ {
+ //Always reply with acknowledgement in order to stop the sender repeatedly sending it
+ WriteBytesToConnection( //TODO group acks together
+ new byte[]
+ {
+ (byte)SendOptionInternal.Acknowledgement,
+ byte1,
+ byte2
+ }
+ );
+ }
}
}
/// <param name="bytes">The bytes to write.</param>
protected abstract void WriteBytesToConnection(byte[] bytes);
+ protected UdpConnection()
+ {
+ InitializeKeepAliveTimer();
+ }
+
/// <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>
+ /// <param name="sendOption">The send option as a byte.</param>
/// <returns>The bytes that should actually be sent.</returns>
- protected void HandleSend(byte[] data, SendOption sendOption)
+ protected void HandleSend(byte[] data, byte sendOption, Action ackCallback = null)
{
byte[] bytes;
switch (sendOption)
{
- case SendOption.Reliable:
+ //Handle reliable header
+ case (byte)SendOption.Reliable:
bytes = new byte[data.Length + 3];
- WriteReliableSendHeader(bytes);
+ WriteReliableSendHeader(bytes, ackCallback);
+ break;
+
+ //Handle hellos (ignore data)
+ case (byte)SendOptionInternal.Hello:
+ bytes = new byte[3];
+ WriteReliableSendHeader(bytes, ackCallback);
break;
default:
}
//Add message type
- bytes[0] = (byte)sendOption;
+ bytes[0] = sendOption;
//Copy data into new array
Buffer.BlockCopy(data, 0, bytes, bytes.Length - data.Length, data.Length);
+ //Inform keepalive not to send for a while
+ ResetKeepAliveTimer(); //TODO keepalive tests
+
//Write to connection
WriteBytesToConnection(bytes);
-
+
Statistics.LogSend(data.Length, bytes.Length);
}
/// <returns>The bytes of data received.</returns>
protected byte[] HandleReceive(byte[] buffer, int bytesReceived)
{
+ //Inform keepalive not to send for a while
+ ResetKeepAliveTimer();
+
int headerSize = 1;
switch (buffer[0])
{
+ //Handle reliable receives
case (byte)SendOption.Reliable:
headerSize = 3;
return null;
break;
+ //Handle acknowledgments
case (byte)SendOptionInternal.Acknowledgement:
HandleAcknowledgement(buffer);
-
- Statistics.LogReceive(0, bytesReceived);
-
+
+ return null;
+
+ //We need to acknowledge hello messages so just use the same reliable receive
+ //method
+ case (byte)SendOptionInternal.Hello:
+ HandleReliableReceive(buffer);
+
return null;
}
return dataBytes;
}
+
+ /// <summary>
+ /// Sends a hello packet to the remote endpoint.
+ /// </summary>
+ /// <param name="acknowledgeCallback">The callback to invoke when the hello packet is acknowledged.</param>
+ protected void SendHello(Action acknowledgeCallback)
+ {
+ HandleSend(new byte[0], (byte)SendOptionInternal.Hello, acknowledgeCallback);
+ }
+
+ /// <summary>
+ /// Called when things are being disposed of
+ /// </summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ DisposeKeepAliveTimer();
+ }
+
+ base.Dispose(disposing);
+ }
}
}
{
connection = new UdpServerConnection(this, remoteEndPoint);
connections.Add(remoteEndPoint, connection);
+
+ //Then ping back an ack to make sure they're happy
+ connection.SendAck(buffer[1], buffer[2]);
}
}
/// </summary>
/// <param name="socket"></param>
internal UdpServerConnection(UdpConnectionListener listener, EndPoint endPoint)
+ : base()
{
this.Listener = listener;
this.RemoteEndPoint = endPoint;
/// <param name="sendOption">The option this data is requested to send with.</param>
public override void WriteBytes(byte[] bytes, SendOption sendOption = SendOption.None)
{
- HandleSend(bytes, sendOption);
+ HandleSend(bytes, (byte)sendOption);
}
/// <summary>