aboutsummaryrefslogtreecommitdiff
path: root/Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs')
-rw-r--r--Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs353
1 files changed, 353 insertions, 0 deletions
diff --git a/Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs b/Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs
new file mode 100644
index 0000000..8e6063d
--- /dev/null
+++ b/Tools/Hazel-Networking/Hazel/Udp/UnityUdpClientConnection.cs
@@ -0,0 +1,353 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+
+namespace Hazel.Udp
+{
+ /// <summary>
+ /// Unity doesn't always get along with thread pools well, so this interface will hopefully suit that case better.
+ /// </summary>
+ /// <inheritdoc/>
+ public class UnityUdpClientConnection : 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;
+
+ private Socket socket;
+
+ public UnityUdpClientConnection(ILogger logger, IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4)
+ : base(logger)
+ {
+ this.EndPoint = remoteEndPoint;
+ this.IPMode = ipMode;
+
+ this.socket = CreateSocket(ipMode);
+ this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true);
+ }
+
+ ~UnityUdpClientConnection()
+ {
+ this.Dispose(false);
+ }
+
+ public void FixedUpdate()
+ {
+ try
+ {
+ ResendPacketsIfNeeded();
+ }
+ catch (Exception e)
+ {
+ this.logger.WriteError("FixedUpdate: " + e);
+ }
+
+ try
+ {
+ ManageReliablePackets();
+ }
+ catch (Exception e)
+ {
+ this.logger.WriteError("FixedUpdate: " + e);
+ }
+ }
+
+ protected virtual void RestartConnection()
+ {
+ }
+
+ protected virtual void ResendPacketsIfNeeded()
+ {
+ }
+
+ /// <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)
+ {
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Synchronously writes the given bytes to the connection.
+ /// </summary>
+ /// <param name="bytes">The bytes to write.</param>
+ protected virtual void WriteBytesToConnectionSync(byte[] bytes, int length)
+ {
+ try
+ {
+ socket.SendTo(
+ bytes,
+ 0,
+ length,
+ SocketFlags.None,
+ EndPoint);
+ }
+ 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);
+ }
+ }
+
+ public override void Connect(byte[] bytes = null, int timeout = 5000)
+ {
+ this.ConnectAsync(bytes);
+ for (int timer = 0; timer < timeout; timer += 100)
+ {
+ if (this.State != ConnectionState.Connecting) return;
+ Thread.Sleep(100);
+
+ // I guess if we're gonna block in Unity, then let's assume no one will pump this for us.
+ this.FixedUpdate();
+ }
+ }
+
+ /// <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);
+ }
+
+ this.RestartConnection();
+
+ 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.InitializeKeepAliveTimer();
+ this.State = ConnectionState.Connected;
+ });
+ }
+
+ /// <summary>
+ /// Instructs the listener to begin listening.
+ /// </summary>
+ void StartListeningForData()
+ {
+ var msg = MessageReader.GetSized(this.ReceiveBufferSize);
+ try
+ {
+ EndPoint ep = this.EndPoint;
+ socket.BeginReceiveFrom(msg.Buffer, 0, msg.Buffer.Length, SocketFlags.None, ref ep, ReadCallback, msg);
+ }
+ catch
+ {
+ msg.Recycle();
+ this.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Called when data has been received by the socket.
+ /// </summary>
+ /// <param name="result">The asyncronous operation's result.</param>
+ void ReadCallback(IAsyncResult result)
+ {
+#if DEBUG
+ if (this.TestLagMs > 0)
+ {
+ Thread.Sleep(this.TestLagMs);
+ }
+#endif
+
+ var msg = (MessageReader)result.AsyncState;
+
+ try
+ {
+ EndPoint ep = this.EndPoint;
+ msg.Length = socket.EndReceiveFrom(result, ref ep);
+ }
+ catch (SocketException e)
+ {
+ msg.Recycle();
+ DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message);
+ return;
+ }
+ catch (ObjectDisposedException)
+ {
+ // Weirdly, it seems that this method can be called twice on the same AsyncState when object is disposed...
+ // So this just keeps us from hitting Duplicate Add errors at the risk of if this is a platform
+ // specific bug, we leak a MessageReader while the socket is disposing. Not a bad trade off.
+ 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;
+ }
+ }
+#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;
+ }
+
+ 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
+ {
+ this.WriteBytesToConnectionSync(bytes, bytes.Length);
+ }
+ 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 { }
+
+ base.Dispose(disposing);
+ }
+ }
+}