/// The state of this connection.
/// </summary>
/// <remarks>
- /// <para>
- /// Connections go round 4 states in their lifetime: they start as <see cref="ConnectionState.NotConnected"/> to
- /// indicate they have no endpoint, calling <see cref="Connect"/> takes them into
- /// <see cref="ConnectionState.Connecting"/>, once they have received confirmation they are connected they enter
- /// <see cref="ConnectionState.Connected"/> and finally calling <see cref="Dispose"/> sets them to
- /// <see cref="ConnectionState.Disconnecting"/> and then the sequence repeats back to
- /// <see cref="ConnectionState.NotConnected"/> once disconnection is complete.
- /// </para>
- /// <para>
- /// Data can only be sent while in <see cref="ConnectionState.Connected"/> and all attempts to send data when
- /// in any other state will throw an InvalidOperationException.
- /// </para>
- /// <para>
- /// All implementers should be aware that when this is set to <see cref="ConnectionState.Connected"/> it will
- /// release all threads that are blocked on <see cref="WaitOnConnect"/>.
- /// </para>
+ /// All implementers should be aware that when this is set to ConnectionState.Connected it will
+ /// release all threads that are blocked on <see cref="WaitOnConnect"/>.
/// </remarks>
public ConnectionState State
{
/// <summary>
/// Connects the connection to a server and begins listening.
+ /// This method blocks and may thrown if there is a problem connecting.
/// </summary>
/// <param name="bytes">The bytes of data to send in the handshake.</param>
/// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
- /// <remarks>
- /// Calling Connect makes the connection attempt to connect to the end point that's specified in the
- /// constructor. This method will block until the connection attempt completes and will throw a
- /// <see cref="HazelException"/> if there is a problem connecting.
- /// </remarks>
public abstract void Connect(byte[] bytes = null, int timeout = 5000);
/// <summary>
/// Connects the connection to a server and begins listening.
+ /// This method does not block.
/// </summary>
/// <param name="bytes">The bytes of data to send in the handshake.</param>
/// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
- /// <remarks>
- /// Calling Connect makes the connection attempt to connect to the end point that's specified in the
- /// constructor. This method will block until the connection attempt completes and will throw a
- /// <see cref="HazelException"/> if there is a problem connecting.
- /// </remarks>
public abstract void ConnectAsync(byte[] bytes = null, int timeout = 5000);
/// <summary>
/// <summary>
/// Invokes the Disconnected event.
/// </summary>
- /// <param name="e">The exception, if any, that occured to cause this.</param>
+ /// <param name="e">The exception, if any, that occurred to cause this.</param>
/// <param name="reader">Extra disconnect data</param>
/// <remarks>
/// Invokes the <see cref="Disconnected"/> event to alert subscribres this connection has been disconnected either
- /// by the end point or because an error occured. If an error occured the error should be passed in in order to
+ /// by the end point or because an error occurred. If an error occurred the error should be passed in in order to
/// pass to the subscribers, otherwise null can be passed in.
/// </remarks>
protected void InvokeDisconnected(string e, MessageReader reader)
/// Blocks until the Connection is connected.
/// </summary>
/// <param name="timeout">The number of milliseconds to wait before timing out.</param>
- /// <remarks>
- /// This is a helper method for waiting until the connection is connected. It will block until the
- /// <see cref="State"/> property is set to <see cref="ConnectionState.Connected"/> allowing the main thread to
- /// wait until specific data is received etc. before returning to the user's code.
- /// </remarks>
protected bool WaitOnConnect(int timeout)
{
return connectWaitLock.WaitOne(timeout);
/// For times when you want to force the disconnect handler to fire as well as close it.
/// If you only want to close it, just use Dispose.
/// </summary>
- public abstract void Disconnect(string reason, MessageWriter writer = null, bool fireEvent = true);
+ public abstract void Disconnect(string reason, MessageWriter writer = null);
/// <summary>
/// Disposes of this NetworkConnection.
/// <remarks>
/// <para>
/// ConnectionListeners are server side objects that listen for clients and create matching server side connections
- /// for each client in a similar way to TCP does. These connections should already have a
- /// <see cref="Connection.State">State</see> of <see cref="ConnectionState.Connected"/> and so should be ready for
- /// comunication immediately.
+ /// for each client in a similar way to TCP does. These connections should be ready for communication immediately.
/// </para>
/// <para>
/// Each time a client connects the <see cref="NewConnection"/> event will be invoked to alert all subscribers to
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
namespace Hazel
{
- /// <summary>
- /// Event arguments for the <see cref="Connection.Disconnected"/> event.
- /// </summary>
- /// <remarks>
- /// <para>
- /// This contains information about the cause of a disconnection and is passed to subscribers of the
- /// <see cref="Connection.Disconnected"/> event.
- /// </para>
- /// <include file="DocInclude/common.xml" path="docs/item[@name='Recyclable']/*" />
- /// </remarks>
- /// <threadsafety static="true" instance="true"/>
public class DisconnectedEventArgs : EventArgs
{
/// <summary>
- /// The exception, if any, that caused the disconnect.
+ /// Optional disconnect reason. May be null.
/// </summary>
- /// <remarks>
- /// If the disconnection was caused because of an exception occuring (for exemple a
- /// <see cref="System.Net.Sockets.SocketException"/> on network based connections) this will contain the error
- /// that caused it or a <see cref="HazelException"/> with the details of the exception, if the disconnection
- /// wasn't caused by an error then this will contain null.
- /// </remarks>
public readonly string Reason;
+ /// <summary>
+ /// Optional data sent with a disconnect message. May be null.
+ /// You must not recycle this. If you need the message outside of a callback, you should copy it.
+ /// </summary>
public readonly MessageReader Message;
public DisconnectedEventArgs(string reason, MessageReader message)
this.Buffer[0] = (byte)sendOption;
switch (sendOption)
{
+ default:
case SendOption.None:
this.Length = this.Position = 1;
break;
this.Write(bytes, length);
}
+ public void WriteBytesAndSize(byte[] bytes, int offset, int length)
+ {
+ this.WritePacked((uint)length);
+ this.Write(bytes, offset, length);
+ }
+
public void Write(byte[] bytes)
{
Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length);
if (this.Position > this.Length) this.Length = this.Position;
}
+ public void Write(byte[] bytes, int offset, int length)
+ {
+ Array.Copy(bytes, offset, this.Buffer, this.Position, length);
+ this.Position += length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
public void Write(byte[] bytes, int length)
{
Array.Copy(bytes, 0, this.Buffer, this.Position, length);
}
#endregion
+ public void Write(MessageWriter msg, bool includeHeader)
+ {
+ int offset = 0;
+ if (!includeHeader)
+ {
+ switch (msg.SendOption)
+ {
+ case SendOption.None:
+ offset = 1;
+ break;
+ case SendOption.Reliable:
+ offset = 3;
+ break;
+ }
+ }
+
+ this.Write(msg.Buffer, offset, msg.Length - offset);
+ }
+
public unsafe static bool IsLittleEndian()
{
byte b;
/// <summary>
/// Called when the socket has been disconnected locally.
/// </summary>
- public override void Disconnect(string reason, MessageWriter writer = null, bool fireEvent = true)
+ public override void Disconnect(string reason, MessageWriter writer = null)
{
- if (this.SendDisconnect(writer) && fireEvent)
+ if (this.SendDisconnect(writer))
{
try
{
/// <returns>An instance of T.</returns>
internal T GetObject()
{
- T item;
- if (!pool.TryTake(out item))
+ if (!pool.TryTake(out T item))
{
Interlocked.Increment(ref numberCreated);
item = objectFactory.Invoke();
throw new Exception("Duplicate add " + typeof(T).Name);
}
}
+
+ public bool IsObjectInUse(T item)
+ {
+ return inuse.ContainsKey(item);
+ }
}
}
/// <summary>
/// Extra internal states for SendOption enumeration when using UDP.
/// </summary>
- enum UdpSendOption : byte
+ public enum UdpSendOption : byte
{
/// <summary>
/// Hello message for initiating communication.
}
catch (SocketException ex)
{
- Disconnect("Could not send data as a SocketException occured: " + ex.Message);
+ Disconnect("Could not send data as a SocketException occurred: " + ex.Message);
}
}
}
catch (SocketException ex)
{
- Disconnect("Could not send data as a SocketException occured: " + ex.Message);
+ Disconnect("Could not send data as a SocketException occurred: " + ex.Message);
}
}
catch (SocketException e)
{
this.State = ConnectionState.NotConnected;
- throw new HazelException("A socket exception occured while binding to the port.", e);
+ throw new HazelException("A SocketException occurred while binding to the port.", e);
}
try
catch (SocketException e)
{
Dispose();
- throw new HazelException("A Socket exception occured while initiating a receive operation.", e);
+ throw new HazelException("A SocketException occurred while initiating a receive operation.", e);
}
// Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present)
// When acknowledged set the state to connected
- SendHello(bytes, () => { this.State = ConnectionState.Connected; });
+ SendHello(bytes, () =>
+ {
+ this.State = ConnectionState.Connected;
+ this.InitializeKeepAliveTimer();
+ });
}
/// <summary>
{
msg.Length = socket.EndReceive(result);
}
- catch (NullReferenceException)
- {
- msg.Recycle();
- return;
- }
- catch (ObjectDisposedException)
+ catch (SocketException e)
{
msg.Recycle();
+ Disconnect("Socket exception while reading data: " + e.Message);
return;
}
- catch (SocketException e)
+ catch (Exception)
{
msg.Recycle();
- Disconnect("Socket exception while reading data: " + e.Message);
return;
}
{
lock (this)
{
- if (this._state != ConnectionState.Connected) return false;
+ if (this._state == ConnectionState.NotConnected) return false;
this._state = ConnectionState.NotConnected;
}
SendDisconnect();
}
- if (this.socket != null)
- {
- try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
- try { this.socket.Close(); } catch { }
- try { this.socket.Dispose(); } catch { }
-
- this.socket = null;
- }
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ try { this.socket.Close(); } catch { }
+ try { this.socket.Dispose(); } catch { }
this.reliablePacketTimer.Dispose();
set
{
keepAliveInterval = value;
-
- //Update timer
ResetKeepAliveTimer();
}
}
- int keepAliveInterval = 1500;
+ private int keepAliveInterval = 1500;
public int MissingPingsUntilDisconnect { get; set; } = 6;
- int pingsSinceAck = 0;
+ private volatile int pingsSinceAck = 0;
/// <summary>
/// The timer creating keepalive pulses.
/// </summary>
- Timer keepAliveTimer;
+ private Timer keepAliveTimer;
/// <summary>
/// Starts the keepalive timer.
/// </summary>
- void InitializeKeepAliveTimer()
+ protected void InitializeKeepAliveTimer()
{
keepAliveTimer = new Timer(
(o) =>
{
if (this.pingsSinceAck >= this.MissingPingsUntilDisconnect)
{
+ this.DisposeKeepAliveTimer();
this.Disconnect($"Sent {this.pingsSinceAck} pings that remote has not responded to.");
return;
}
try
{
- SendPing();
this.pingsSinceAck++;
+ SendPing();
}
catch
{
- DisposeKeepAliveTimer();
}
},
null,
// An unacked ping should never be the sole cause of a disconnect.
// Rather, the responses will reset our pingsSinceAck, enough unacked
// pings should cause a disconnect.
- void SendPing()
+ private void SendPing()
{
ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated);
/// <summary>
/// Resets the keepalive timer to zero.
/// </summary>
- void ResetKeepAliveTimer()
+ private void ResetKeepAliveTimer()
{
try
{
/// <summary>
/// Disposes of the keep alive timer.
/// </summary>
- void DisposeKeepAliveTimer()
+ private void DisposeKeepAliveTimer()
{
- var timer = this.keepAliveTimer;
- if (timer != null)
+ if (this.keepAliveTimer != null)
{
- this.keepAliveTimer = null;
- timer.Dispose();
+ this.keepAliveTimer.Dispose();
}
foreach (var kvp in activePingPackets)
/// This returns the average ping for a one-way trip as calculated from the reliable packets that have been sent
/// and acknowledged by the endpoint.
/// </remarks>
- public float AveragePingMs = 200;
+ public float AveragePingMs = 500;
/// <summary>
/// The maximum times a message should be resent before marking the endpoint as disconnected.
return 0;
}
- this.NextTimeout = (int)Math.Min(this.NextTimeout * connection.ResendPingMultiplier, 1500);
+ this.NextTimeout += (int)Math.Min(this.NextTimeout * connection.ResendPingMultiplier, 500);
try
{
connection.WriteBytesToConnection(this.Data, this.Length);
/// <param name="buffer">The buffer to attach to.</param>
/// <param name="offset">The offset to attach at.</param>
/// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param>
- void AttachReliableID(byte[] buffer, int offset, int sendLength, Action ackCallback = null)
+ private void AttachReliableID(byte[] buffer, int offset, int sendLength, Action ackCallback = null)
{
ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated);
/// <param name="sendOption"></param>
/// <param name="data">The byte array to write to.</param>
/// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param>
- void ReliableSend(byte sendOption, byte[] data, Action ackCallback = null)
+ private void ReliableSend(byte sendOption, byte[] data, Action ackCallback = null)
{
this.ReliableSend(sendOption, data, 0, data.Length, ackCallback);
}
/// <param name="offset"></param>
/// <param name="length"></param>
/// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param>
- void ReliableSend(byte sendOption, byte[] data, int offset, int length, Action ackCallback = null)
+ private void ReliableSend(byte sendOption, byte[] data, int offset, int length, Action ackCallback = null)
{
//Inform keepalive not to send for a while
ResetKeepAliveTimer();
/// Handles a reliable message being received and invokes the data event.
/// </summary>
/// <param name="message">The buffer received.</param>
- void ReliableMessageReceive(MessageReader message, int bytesReceived)
+ private void ReliableMessageReceive(MessageReader message, int bytesReceived)
{
ushort id;
if (ProcessReliableReceive(message.Buffer, 1, out id))
/// <param name="bytes">The buffer containing the data.</param>
/// <param name="offset">The offset of the reliable header.</param>
/// <returns>Whether the packet was a new packet or not.</returns>
- bool ProcessReliableReceive(byte[] bytes, int offset, out ushort id)
+ private bool ProcessReliableReceive(byte[] bytes, int offset, out ushort id)
{
byte b1 = bytes[offset];
byte b2 = bytes[offset + 1];
/// Handles acknowledgement packets to us.
/// </summary>
/// <param name="bytes">The buffer containing the data.</param>
- void AcknowledgementMessageReceive(byte[] bytes)
+ private void AcknowledgementMessageReceive(byte[] bytes)
{
this.pingsSinceAck = 0;
/// </summary>
/// <param name="byte1">The first identification byte.</param>
/// <param name="byte2">The second identification byte.</param>
- internal void SendAck(byte byte1, byte byte2)
+ private void SendAck(byte byte1, byte byte2)
{
byte[] bytes = new byte[]
{
catch (InvalidOperationException) { }
}
- void DisposeReliablePackets()
+ private void DisposeReliablePackets()
{
foreach (var kvp in reliableDataPacketsSent)
{
{
protected static readonly byte[] EmptyDisconnectBytes = new byte[] { (byte)UdpSendOption.Disconnect };
- /// <summary>
- /// Creates a new UdpConnection and initializes the keep alive timer.
- /// </summary>
- protected UdpConnection()
- {
- InitializeKeepAliveTimer();
- }
-
/// <summary>
/// Writes the given bytes to the connection.
/// </summary>
switch (msg.SendOption)
{
case SendOption.Reliable:
- // Inform keepalive not to send for a while
ResetKeepAliveTimer();
+
AttachReliableID(buffer, 1, buffer.Length);
WriteBytesToConnection(buffer, buffer.Length);
Statistics.LogReliableSend(buffer.Length - 3, buffer.Length);
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
-using System.Text;
using System.Threading;
namespace Hazel.Udp
public int MinConnectionLength = 0;
+ public delegate bool AcceptConnectionCheck(out byte[] response);
+ public AcceptConnectionCheck AcceptConnection;
+
/// <summary>
/// The socket listening for connections.
/// </summary>
}
catch (SocketException e)
{
- throw new HazelException("Could not start listening as a SocketException occured", e);
+ throw new HazelException("Could not start listening as a SocketException occurred", e);
}
StartListeningForData();
message = MessageReader.GetSized(BufferSize);
socket.BeginReceiveFrom(message.Buffer, 0, message.Buffer.Length, SocketFlags.None, ref remoteEP, ReadCallback, message);
- Interlocked.Increment(ref ActiveListeners);
}
- catch (SocketException)
+ catch (SocketException sx)
{
message?.Recycle();
+
+ this.Logger?.Invoke("Socket Ex in StartListening: " + sx.Message);
+
+ Thread.Sleep(10);
StartListeningForData();
return;
}
return;
}
}
-
- /// <summary>
- /// Called when data has been received by the listener.
- /// </summary>
- /// <param name="result">The asyncronous operation's result.</param>
-
- public int ActiveListeners;
- public int PacketsReceived;
+ public volatile int ActiveCallbacks;
void ReadCallback(IAsyncResult result)
{
- Interlocked.Decrement(ref ActiveListeners);
- Interlocked.Increment(ref PacketsReceived);
-
+ Interlocked.Increment(ref this.ActiveCallbacks);
var message = (MessageReader)result.AsyncState;
int bytesReceived;
EndPoint remoteEndPoint = new IPEndPoint(IPMode == IPMode.IPv4 ? IPAddress.Any : IPAddress.IPv6Any, 0);
message.Offset = 0;
message.Length = bytesReceived;
}
- catch (SocketException)
+ catch (SocketException sx)
{
// Client no longer reachable, pretend it didn't happen
// TODO should this not inform the connection this client is lost???
// This thread suggests the IP is not passed out from WinSoc so maybe not possible
// http://stackoverflow.com/questions/2576926/python-socket-error-on-udp-data-receive-10054
message.Recycle();
-
+ this.Logger?.Invoke("Socket Ex in ReadCallback: " + sx.Message);
+
+ Thread.Sleep(10);
StartListeningForData();
+ Interlocked.Decrement(ref this.ActiveCallbacks);
return;
}
catch (Exception ex)
//If the socket's been disposed then we can just end there.
message.Recycle();
this.Logger?.Invoke("Stopped due to: " + ex.Message);
+ Interlocked.Decrement(ref this.ActiveCallbacks);
return;
}
if (bytesReceived == 0)
{
message.Recycle();
+ this.Logger?.Invoke("Received 0 bytes");
+ Thread.Sleep(10);
StartListeningForData();
+ Interlocked.Decrement(ref this.ActiveCallbacks);
return;
}
if (!isHello)
{
message.Recycle();
+ Interlocked.Decrement(ref this.ActiveCallbacks);
return;
}
- lock (this.allConnections)
+ if (AcceptConnection != null)
{
- aware = this.allConnections.TryGetValue(remoteEndPoint, out connection);
- if (!aware)
+ if (!AcceptConnection(out var response))
{
- connection = new UdpServerConnection(this, (IPEndPoint)remoteEndPoint, this.IPMode);
- if (!this.allConnections.TryAdd(remoteEndPoint, connection))
- {
- throw new Exception();
- }
+ message.Recycle();
+ SendData(response, response.Length, remoteEndPoint);
+ Interlocked.Decrement(ref this.ActiveCallbacks);
+ return;
}
}
+
+ connection = this.allConnections.GetOrAdd(remoteEndPoint, (ep) =>
+ {
+ aware = false;
+ return new UdpServerConnection(this, (IPEndPoint)ep, this.IPMode);
+ });
}
//Inform the connection of the buffer (new connections need to send an ack back to client)
{
message.Recycle();
}
+
+ Interlocked.Decrement(ref this.ActiveCallbacks);
}
#if DEBUG
}
catch (SocketException e)
{
- throw new HazelException("Could not send data as a SocketException occured.", e);
+ throw new HazelException("Could not send data as a SocketException occurred.", e);
}
catch (ObjectDisposedException)
{
kvp.Value.Dispose();
}
- if (this.socket != null)
- {
- try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
- try { this.socket.Close(); } catch { }
- try { this.socket.Dispose(); } catch { }
- this.socket = null;
- }
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ try { this.socket.Close(); } catch { }
+ try { this.socket.Dispose(); } catch { }
this.reliablePacketTimer.Dispose();
this.IPMode = IPMode;
State = ConnectionState.Connected;
+ this.InitializeKeepAliveTimer();
}
/// <inheritdoc />
SendDisconnect();
}
-
base.Dispose(disposing);
}
}