aboutsummaryrefslogtreecommitdiff
path: root/Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs')
-rw-r--r--Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs364
1 files changed, 364 insertions, 0 deletions
diff --git a/Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs b/Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs
new file mode 100644
index 0000000..f6da329
--- /dev/null
+++ b/Tools/Hazel-Networking/Hazel/Udp/UdpClientConnection.cs
@@ -0,0 +1,364 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+
+namespace Hazel.Udp
+{
+ /// <summary>
+ /// Represents a client's connection to a server that uses the UDP protocol.
+ /// </summary>
+ /// <inheritdoc/>
+ public sealed class UdpClientConnection : UdpConnection
+ {
+ /// <summary>
+ /// The max size Hazel attempts to read from the network.
+ /// Defaults to 8096.
+ /// </summary>
+ /// <remarks>
+ /// 8096 is 5 times the standard modern MTU of 1500, so it's already too large imo.
+ /// If Hazel ever implements fragmented packets, then we might consider a larger value since combining 5
+ /// packets into 1 reader would be realistic and would cause reallocations. That said, Hazel is not meant
+ /// for transferring large contiguous blocks of data, so... please don't?
+ /// </remarks>
+ public int ReceiveBufferSize = 8096;
+
+ /// <summary>
+ /// The socket we're connected via.
+ /// </summary>
+ private Socket socket;
+
+ /// <summary>
+ /// Reset event that is triggered when the connection is marked Connected.
+ /// </summary>
+ private ManualResetEvent connectWaitLock = new ManualResetEvent(false);
+
+ private Timer reliablePacketTimer;
+
+#if DEBUG
+ public event Action<byte[], int> DataSentRaw;
+ public event Action<byte[], int> DataReceivedRaw;
+#endif
+
+ /// <summary>
+ /// Creates a new UdpClientConnection.
+ /// </summary>
+ /// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
+ public UdpClientConnection(ILogger logger, IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4)
+ : base(logger)
+ {
+ this.EndPoint = remoteEndPoint;
+ this.IPMode = ipMode;
+
+ this.socket = CreateSocket(ipMode);
+
+ reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite);
+ this.InitializeKeepAliveTimer();
+ }
+
+ ~UdpClientConnection()
+ {
+ this.Dispose(false);
+ }
+
+ private void ManageReliablePacketsInternal(object state)
+ {
+ base.ManageReliablePackets();
+ try
+ {
+ reliablePacketTimer.Change(100, Timeout.Infinite);
+ }
+ catch { }
+ }
+
+ /// <inheritdoc />
+ protected override void WriteBytesToConnection(byte[] bytes, int length)
+ {
+#if DEBUG
+ if (TestLagMs > 0)
+ {
+ ThreadPool.QueueUserWorkItem(a => { Thread.Sleep(this.TestLagMs); WriteBytesToConnectionReal(bytes, length); });
+ }
+ else
+#endif
+ {
+ WriteBytesToConnectionReal(bytes, length);
+ }
+ }
+
+ private void WriteBytesToConnectionReal(byte[] bytes, int length)
+ {
+#if DEBUG
+ DataSentRaw?.Invoke(bytes, length);
+#endif
+
+ try
+ {
+ this.Statistics.LogPacketSend(length);
+ socket.BeginSendTo(
+ bytes,
+ 0,
+ length,
+ SocketFlags.None,
+ EndPoint,
+ HandleSendTo,
+ null);
+ }
+ catch (NullReferenceException) { }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed and disconnected...
+ }
+ catch (SocketException ex)
+ {
+ DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message);
+ }
+ }
+
+ private void HandleSendTo(IAsyncResult result)
+ {
+ try
+ {
+ socket.EndSendTo(result);
+ }
+ catch (NullReferenceException) { }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed and disconnected...
+ }
+ catch (SocketException ex)
+ {
+ DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void Connect(byte[] bytes = null, int timeout = 5000)
+ {
+ this.ConnectAsync(bytes);
+
+ //Wait till hello packet is acknowledged and the state is set to Connected
+ bool timedOut = !WaitOnConnect(timeout);
+
+ //If we timed out raise an exception
+ if (timedOut)
+ {
+ Dispose();
+ throw new HazelException("Connection attempt timed out.");
+ }
+ }
+
+ /// <inheritdoc />
+ public override void ConnectAsync(byte[] bytes = null)
+ {
+ this.State = ConnectionState.Connecting;
+
+ try
+ {
+ if (IPMode == IPMode.IPv4)
+ socket.Bind(new IPEndPoint(IPAddress.Any, 0));
+ else
+ socket.Bind(new IPEndPoint(IPAddress.IPv6Any, 0));
+ }
+ catch (SocketException e)
+ {
+ this.State = ConnectionState.NotConnected;
+ throw new HazelException("A SocketException occurred while binding to the port.", e);
+ }
+
+ try
+ {
+ StartListeningForData();
+ }
+ catch (ObjectDisposedException)
+ {
+ // If the socket's been disposed then we can just end there but make sure we're in NotConnected state.
+ // If we end up here I'm really lost...
+ this.State = ConnectionState.NotConnected;
+ return;
+ }
+ catch (SocketException e)
+ {
+ Dispose();
+ 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;
+ this.InitializeKeepAliveTimer();
+ });
+ }
+
+ /// <summary>
+ /// Instructs the listener to begin listening.
+ /// </summary>
+ void StartListeningForData()
+ {
+#if DEBUG
+ if (this.TestLagMs > 0)
+ {
+ Thread.Sleep(this.TestLagMs);
+ }
+#endif
+
+ var msg = MessageReader.GetSized(this.ReceiveBufferSize);
+ try
+ {
+ socket.BeginReceive(msg.Buffer, 0, msg.Buffer.Length, SocketFlags.None, ReadCallback, msg);
+ }
+ catch
+ {
+ msg.Recycle();
+ this.Dispose();
+ }
+ }
+
+ protected override void SetState(ConnectionState state)
+ {
+ try
+ {
+ // If the server disconnects you during the hello
+ // you can go straight from Connecting to NotConnected.
+ if (state == ConnectionState.Connected
+ || state == ConnectionState.NotConnected)
+ {
+ connectWaitLock.Set();
+ }
+ else
+ {
+ connectWaitLock.Reset();
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ /// <summary>
+ /// Blocks until the Connection is connected.
+ /// </summary>
+ /// <param name="timeout">The number of milliseconds to wait before timing out.</param>
+ public bool WaitOnConnect(int timeout)
+ {
+ return connectWaitLock.WaitOne(timeout);
+ }
+
+ /// <summary>
+ /// Called when data has been received by the socket.
+ /// </summary>
+ /// <param name="result">The asyncronous operation's result.</param>
+ void ReadCallback(IAsyncResult result)
+ {
+ var msg = (MessageReader)result.AsyncState;
+
+ try
+ {
+ msg.Length = socket.EndReceive(result);
+ }
+ catch (SocketException e)
+ {
+ msg.Recycle();
+ DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message);
+ return;
+ }
+ catch (Exception)
+ {
+ msg.Recycle();
+ return;
+ }
+
+ //Exit if no bytes read, we've failed.
+ if (msg.Length == 0)
+ {
+ msg.Recycle();
+ DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes");
+ return;
+ }
+
+ //Begin receiving again
+ try
+ {
+ StartListeningForData();
+ }
+ catch (SocketException e)
+ {
+ DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception during receive: " + e.Message);
+ }
+ catch (ObjectDisposedException)
+ {
+ //If the socket's been disposed then we can just end there.
+ return;
+ }
+
+#if DEBUG
+ if (this.TestDropRate > 0)
+ {
+ if ((this.testDropCount++ % this.TestDropRate) == 0)
+ {
+ return;
+ }
+ }
+
+ DataReceivedRaw?.Invoke(msg.Buffer, msg.Length);
+#endif
+ HandleReceive(msg, msg.Length);
+ }
+
+ /// <summary>
+ /// Sends a disconnect message to the end point.
+ /// You may include optional disconnect data. The SendOption must be unreliable.
+ /// </summary>
+ protected override bool SendDisconnect(MessageWriter data = null)
+ {
+ lock (this)
+ {
+ if (this._state == ConnectionState.NotConnected) return false;
+ this.State = ConnectionState.NotConnected; // Use the property so we release the state lock
+ }
+
+ var bytes = EmptyDisconnectBytes;
+ if (data != null && data.Length > 0)
+ {
+ if (data.SendOption != SendOption.None) throw new ArgumentException("Disconnect messages can only be unreliable.");
+
+ bytes = data.ToByteArray(true);
+ bytes[0] = (byte)UdpSendOption.Disconnect;
+ }
+
+ try
+ {
+ socket.SendTo(
+ bytes,
+ 0,
+ bytes.Length,
+ SocketFlags.None,
+ EndPoint);
+ }
+ catch { }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ SendDisconnect();
+ }
+
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ try { this.socket.Close(); } catch { }
+ try { this.socket.Dispose(); } catch { }
+
+ this.reliablePacketTimer.Dispose();
+ this.connectWaitLock.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+}