]> git.deb.at Git - rhonda/impostor.hazel.git/commitdiff
Create a separate client connection better suited to unity. Tidy up a great many...
authorForest <forest@innersloth.com>
Tue, 17 Dec 2019 21:05:31 +0000 (13:05 -0800)
committerForest <forest@innersloth.com>
Tue, 17 Dec 2019 21:05:31 +0000 (13:05 -0800)
Hazel/Connection.cs
Hazel/ConnectionStatistics.cs
Hazel/Hazel.csproj
Hazel/NewConnectionEventArgs.cs
Hazel/Udp/UdpBroadcastListener.cs
Hazel/Udp/UdpClientConnection.cs
Hazel/Udp/UdpConnection.cs
Hazel/Udp/UdpConnectionListener.cs
Hazel/Udp/UnityUdpClientConnection.cs [new file with mode: 0644]

index 2e68f738fc5e30aea524b6476b4a6987b6c6ebb9..f06a15f4742291d84939eb932c69f9f3666dc3bc 100644 (file)
@@ -107,20 +107,13 @@ namespace Hazel
             protected set
             {
                 this._state = value;
-                if (this._state == ConnectionState.Connected)
-                    connectWaitLock.Set();
-                else
-                    connectWaitLock.Reset();
+                this.SetState(value);
             }
         }
 
         protected ConnectionState _state;
-
-        /// <summary>
-        ///     Reset event that is triggered when the connection is marked Connected.
-        /// </summary>
-        private ManualResetEvent connectWaitLock = new ManualResetEvent(false);
-
+        protected virtual void SetState(ConnectionState state) { }
+        
         /// <summary>
         ///     Constructor that initializes the ConnecitonStatistics object.
         /// </summary>
@@ -225,15 +218,6 @@ namespace Hazel
             }
         }
 
-        /// <summary>
-        ///     Blocks until the Connection is connected.
-        /// </summary>
-        /// <param name="timeout">The number of milliseconds to wait before timing out.</param>
-        protected bool WaitOnConnect(int timeout)
-        {
-            return connectWaitLock.WaitOne(timeout);
-        }
-
         /// <summary>
         /// 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.
@@ -259,7 +243,6 @@ namespace Hazel
             {
                 this.DataReceived = null;
                 this.Disconnected = null;
-                this.connectWaitLock.Dispose();
             }
         }
     }
index c183062744dae0fad4c4f9bc893db3ca581fea91..1e90105aa0420581a6db17868634d8c5e47e51aa 100644 (file)
@@ -25,7 +25,7 @@ namespace Hazel
         }
 
         /// <summary>
-        ///     The number of messages sent larger than 1400 bytes. This is smaller than most default MTUs.
+        ///     The number of messages sent larger than 576 bytes. This is smaller than most default MTUs.
         /// </summary>
         /// <remarks>
         ///     This is the number of unreliable messages that were sent from the <see cref="Connection"/>, incremented 
@@ -41,7 +41,7 @@ namespace Hazel
         }
 
         /// <summary>
-        ///     The number of messages sent larger than 1400 bytes.
+        ///     The number of messages sent larger than 576 bytes.
         /// </summary>
         int fragmentableMessagesSent;
 
@@ -403,7 +403,7 @@ namespace Hazel
             Interlocked.Add(ref dataBytesSent, dataLength);
             Interlocked.Add(ref totalBytesSent, totalLength);
 
-            if (totalLength > 1400)
+            if (totalLength > 576)
             {
                 Interlocked.Increment(ref fragmentableMessagesSent);
             }
index 5114784dfffb8f62f816880e9228ae94e3fef72c..07937143811987d340127e04eb025024b75adc07 100644 (file)
@@ -87,6 +87,7 @@
     <Compile Include="ConnectionStatistics.cs" />
     <Compile Include="Udp\UdpBroadcaster.cs" />
     <Compile Include="Udp\UdpBroadcastListener.cs" />
+    <Compile Include="Udp\UnityUdpClientConnection.cs" />
     <Compile Include="Udp\UdpClientConnection.cs" />
     <Compile Include="Udp\UdpConnection.cs">
       <SubType>Code</SubType>
index 3c7ea9ca9966b4a4f055fb3f0a8fef3d7c7b6052..68fd37f15896b8260a85c42234a121373b7eef38 100644 (file)
@@ -4,6 +4,7 @@
     {
         /// <summary>
         /// The data received from the client in the handshake.
+        /// This data is yours. Remember to recycle it.
         /// </summary>
         public readonly MessageReader HandshakeData;
 
index 973f63e8def0ccd787cac129e85d18343c85914b..13b8d0beb9f38ef767a7bb7f1387c84d76121122 100644 (file)
@@ -58,11 +58,7 @@ namespace Hazel.Udp
             try
             {
                 EndPoint endpt = new IPEndPoint(IPAddress.Any, 0);
-                var result = this.socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpt, this.HandleData, null);
-                if (result.CompletedSynchronously)
-                {
-                    ThreadPool.QueueUserWorkItem(_ => this.HandleData(result));
-                }
+                this.socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpt, this.HandleData, null);
             }
             catch (NullReferenceException) { }
             catch (Exception e)
index 503025da33502a27d7cef61e92d98ffad57dab2f..1b3a22bf20aa7d6b160d07c2a8ad7ba6c9c37df4 100644 (file)
@@ -17,6 +17,11 @@ namespace Hazel.Udp
         /// </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
@@ -35,16 +40,7 @@ namespace Hazel.Udp
             this.RemoteEndPoint = remoteEndPoint;
             this.IPMode = ipMode;
 
-            if (this.IPMode == IPMode.IPv4)
-                socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-            else
-            {
-                if (!Socket.OSSupportsIPv6)
-                    throw new InvalidOperationException("IPV6 not supported!");
-
-                socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
-                socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
-            }
+            this.socket = CreateSocket(ipMode);
 
             reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite);
         }
@@ -204,9 +200,27 @@ namespace Hazel.Udp
             catch
             {
                 msg.Recycle();
+                this.Dispose();
             }
         }
 
+        protected override void SetState(ConnectionState state)
+        {
+            if (state == ConnectionState.Connected)
+                connectWaitLock.Set();
+            else
+                connectWaitLock.Reset();
+        }
+
+        /// <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>
@@ -316,6 +330,7 @@ namespace Hazel.Udp
             try { this.socket.Dispose(); } catch { }
 
             this.reliablePacketTimer.Dispose();
+            this.connectWaitLock.Dispose();
 
             base.Dispose(disposing);
         }
index 07b66e768f148ad31a2a3ec0863be6122d4bc749..083cb725234a9c11c0336d52801b704ffa760a26 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using System.Net.Sockets;
 
 namespace Hazel.Udp
 {
@@ -8,8 +9,43 @@ namespace Hazel.Udp
     /// <inheritdoc />
     public abstract partial class UdpConnection : NetworkConnection
     {
+        private const int SioUdpConnectionReset = -1744830452;
+
         public static readonly byte[] EmptyDisconnectBytes = new byte[] { (byte)UdpSendOption.Disconnect };
 
+        internal static Socket CreateSocket(IPMode ipMode)
+        {
+            Socket socket;
+            if (ipMode == IPMode.IPv4)
+            {
+                socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+            }
+            else
+            {
+                if (!Socket.OSSupportsIPv6)
+                    throw new InvalidOperationException("IPV6 not supported!");
+
+                socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
+                socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
+            }
+
+            try
+            {
+                socket.DontFragment = false;
+            }
+            catch { }
+
+
+            try
+            {
+                const int SIO_UDP_CONNRESET = -1744830452;
+                socket.IOControl(SIO_UDP_CONNRESET, new byte[1], null);
+            }
+            catch { } // Only necessary on Windows
+
+            return socket;
+        }
+
         /// <summary>
         ///     Writes the given bytes to the connection.
         /// </summary>
index 3d67fda84d862276980e76d234bdae185b7a0d35..9bf15e1a9c28d338c87ba918ab06f2f81b9bfd84 100644 (file)
@@ -12,25 +12,21 @@ namespace Hazel.Udp
     /// <inheritdoc />
     public class UdpConnectionListener : NetworkConnectionListener
     {
-        public const int BufferSize = ushort.MaxValue;
-
-        public int MinConnectionLength = 0;
-
-        public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response);
-        public AcceptConnectionCheck AcceptConnection;
+        private const int SendReceiveBufferSize = 1024 * 1024;
+        private const int BufferSize = ushort.MaxValue;
 
         /// <summary>
-        ///     The socket listening for connections.
+        /// A callback for early connection rejection. 
+        /// * Return false to reject connection.
+        /// * A null response is ok, we just won't send anything.
         /// </summary>
-        Socket socket;
+        public AcceptConnectionCheck AcceptConnection;
+        public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response);
 
+        private Socket socket;
         private Action<string> Logger;
+        private Timer reliablePacketTimer;
 
-        Timer reliablePacketTimer;
-
-        /// <summary>
-        ///     The connections we currently hold
-        /// </summary>
         private ConcurrentDictionary<EndPoint, UdpServerConnection> allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection>();
         
         public int ConnectionCount { get { return this.allConnections.Count; } }
@@ -45,19 +41,10 @@ namespace Hazel.Udp
             this.EndPoint = endPoint;
             this.IPMode = ipMode;
 
-            if (this.IPMode == IPMode.IPv4)
-                this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-            else
-            {
-                if (!Socket.OSSupportsIPv6)
-                    throw new HazelException("IPV6 not supported!");
+            this.socket = UdpConnection.CreateSocket(this.IPMode);
 
-                this.socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
-                this.socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
-            }
-
-            socket.ReceiveBufferSize = BufferSize;
-            socket.SendBufferSize = BufferSize;
+            socket.ReceiveBufferSize = SendReceiveBufferSize;
+            socket.SendBufferSize = SendReceiveBufferSize;
             
             reliablePacketTimer = new Timer(ManageReliablePackets, null, 100, Timeout.Infinite);
         }
@@ -109,11 +96,12 @@ namespace Hazel.Udp
             {
                 message = MessageReader.GetSized(BufferSize);
 
-                var result = socket.BeginReceiveFrom(message.Buffer, 0, message.Buffer.Length, SocketFlags.None, ref remoteEP, ReadCallback, message);
-                if (result.CompletedSynchronously)
-                {
-                    this.Logger("Operation completed synchronously");
-                }
+                socket.BeginReceiveFrom(message.Buffer, 0, message.Buffer.Length, SocketFlags.None, ref remoteEP, ReadCallback, message);
+            }
+            catch (ObjectDisposedException)
+            {
+                message.Recycle();
+                return;
             }
             catch (SocketException sx)
             {
@@ -127,17 +115,14 @@ namespace Hazel.Udp
             }
             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);
                 return;
             }
         }
 
-        public volatile int ActiveCallbacks;
         void ReadCallback(IAsyncResult result)
         {
-            Interlocked.Increment(ref this.ActiveCallbacks);
             var message = (MessageReader)result.AsyncState;
             int bytesReceived;
             EndPoint remoteEndPoint = new IPEndPoint(IPMode == IPMode.IPv4 ? IPAddress.Any : IPAddress.IPv6Any, 0);
@@ -150,6 +135,12 @@ namespace Hazel.Udp
                 message.Offset = 0;
                 message.Length = bytesReceived;
             }
+            catch (ObjectDisposedException)
+            {
+                message.Recycle();
+                return;
+            }
+            catch (InvalidOperationException) { return; } // Callback called twice, somehow...
             catch (SocketException sx)
             {
                 // Client no longer reachable, pretend it didn't happen
@@ -158,11 +149,10 @@ namespace Hazel.Udp
                 // 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);
+                this.Logger?.Invoke($"Socket Ex {sx.SocketErrorCode} in ReadCallback: {sx.Message}");
 
                 Thread.Sleep(10);
                 StartListeningForData();
-                Interlocked.Decrement(ref this.ActiveCallbacks);
                 return;
             }
             catch (Exception ex)
@@ -170,7 +160,6 @@ namespace Hazel.Udp
                 //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;
             }
 
@@ -182,7 +171,6 @@ namespace Hazel.Udp
                 this.Logger?.Invoke("Received 0 bytes");
                 Thread.Sleep(10);
                 StartListeningForData();
-                Interlocked.Decrement(ref this.ActiveCallbacks);
                 return;
             }
 
@@ -190,11 +178,10 @@ namespace Hazel.Udp
             StartListeningForData();
 
             bool aware = true;
-            bool hasHelloByte = message.Buffer[0] == (byte)UdpSendOption.Hello;
-            bool isHello = hasHelloByte && message.Length >= MinConnectionLength;
+            bool isHello = message.Buffer[0] == (byte)UdpSendOption.Hello;
 
-            //If we're aware of this connection use the one already
-            //If this is a new client then connect with them!
+            // If we're aware of this connection use the one already
+            // If this is a new client then connect with them!
             UdpServerConnection connection;
             if (!this.allConnections.TryGetValue(remoteEndPoint, out connection))
             {
@@ -202,11 +189,10 @@ namespace Hazel.Udp
                 {
                     if (!this.allConnections.TryGetValue(remoteEndPoint, out connection))
                     {
-                        //Check for malformed connection attempts
+                        // Check for malformed connection attempts
                         if (!isHello)
                         {
                             message.Recycle();
-                            Interlocked.Decrement(ref this.ActiveCallbacks);
                             return;
                         }
 
@@ -215,8 +201,11 @@ namespace Hazel.Udp
                             if (!AcceptConnection((IPEndPoint)remoteEndPoint, message.Buffer, out var response))
                             {
                                 message.Recycle();
-                                SendData(response, response.Length, remoteEndPoint);
-                                Interlocked.Decrement(ref this.ActiveCallbacks);
+                                if (response != null)
+                                {
+                                    SendData(response, response.Length, remoteEndPoint);
+                                }
+
                                 return;
                             }
                         }
@@ -243,12 +232,10 @@ namespace Hazel.Udp
                 message.Position = 0;
                 InvokeNewConnection(message, connection);
             }
-            else if (isHello || (!isHello && hasHelloByte))
+            else if (isHello)
             {
                 message.Recycle();
             }
-
-            Interlocked.Decrement(ref this.ActiveCallbacks);
         }
 
 #if DEBUG
@@ -284,8 +271,7 @@ namespace Hazel.Udp
                     SocketFlags.None,
                     endPoint,
                     SendCallback,
-                    null
-                );
+                    null);
             }
             catch (SocketException e)
             {
diff --git a/Hazel/Udp/UnityUdpClientConnection.cs b/Hazel/Udp/UnityUdpClientConnection.cs
new file mode 100644 (file)
index 0000000..e067903
--- /dev/null
@@ -0,0 +1,252 @@
+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 class UnityUdpClientConnection : UdpConnection
+    {
+        /// <summary>
+        ///     The socket we're connected via.
+        /// </summary>
+        private Socket socket;
+
+        /// <summary>
+        ///     Creates a new UdpClientConnection.
+        /// </summary>
+        /// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
+        public UnityUdpClientConnection(IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4)
+            : base()
+        {
+            this.EndPoint = remoteEndPoint;
+            this.RemoteEndPoint = remoteEndPoint;
+            this.IPMode = ipMode;
+
+            this.socket = CreateSocket(ipMode);
+        }
+        
+        ~UnityUdpClientConnection()
+        {
+            this.Dispose(false);
+        }
+
+        public void FixedUpdate()
+        {
+            base.ManageReliablePackets();
+        }
+
+        /// <inheritdoc />
+        protected override void WriteBytesToConnection(byte[] bytes, int length)
+        {
+            try
+            {
+                socket.BeginSendTo(
+                    bytes,
+                    0,
+                    length,
+                    SocketFlags.None,
+                    RemoteEndPoint,
+                    HandleSendTo,
+                    null);
+            }
+            catch (NullReferenceException) { }
+            catch (ObjectDisposedException)
+            {
+                // Already disposed and disconnected...
+            }
+            catch (SocketException ex)
+            {
+                Disconnect("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)
+            {
+                Disconnect("Could not send data as a SocketException occurred: " + ex.Message);
+            }
+        }
+
+        public override void Connect(byte[] bytes = null, int timeout = 5000)
+        {
+            throw new NotImplementedException("Use ConnectAsync and check State != ConnectionState.Connecting instead.");
+        }
+
+        /// <inheritdoc />
+        public override void ConnectAsync(byte[] bytes = null, int timeout = 5000)
+        {
+            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()
+        {
+            var msg = MessageReader.GetSized(ushort.MaxValue);
+            try
+            {
+                socket.BeginReceive(msg.Buffer, 0, msg.Buffer.Length, SocketFlags.None, 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)
+        {
+            var msg = (MessageReader)result.AsyncState;
+
+            try
+            {
+                msg.Length = socket.EndReceive(result);
+            }
+            catch (SocketException e)
+            {
+                msg.Recycle();
+                Disconnect("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();
+                Disconnect("Received 0 bytes");
+                return;
+            }
+
+            //Begin receiving again
+            try
+            {
+                StartListeningForData();
+            }
+            catch (SocketException e)
+            {
+                Disconnect("Socket exception during receive: " + e.Message);
+            }
+            catch (ObjectDisposedException)
+            {
+                //If the socket's been disposed then we can just end there.
+                return;
+            }
+
+            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
+            {
+                socket.SendTo(
+                    bytes,
+                    0,
+                    bytes.Length,
+                    SocketFlags.None,
+                    RemoteEndPoint);
+            }
+            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);
+        }
+    }
+}
\ No newline at end of file