From dab39579be0bde34a15d5cb0ba246ce0710599a4 Mon Sep 17 00:00:00 2001 From: Forest Date: Tue, 17 Dec 2019 13:05:31 -0800 Subject: [PATCH] Create a separate client connection better suited to unity. Tidy up a great many things --- Hazel/Connection.cs | 23 +-- Hazel/ConnectionStatistics.cs | 6 +- Hazel/Hazel.csproj | 1 + Hazel/NewConnectionEventArgs.cs | 1 + Hazel/Udp/UdpBroadcastListener.cs | 6 +- Hazel/Udp/UdpClientConnection.cs | 35 +++- Hazel/Udp/UdpConnection.cs | 36 ++++ Hazel/Udp/UdpConnectionListener.cs | 86 ++++----- Hazel/Udp/UnityUdpClientConnection.cs | 252 ++++++++++++++++++++++++++ 9 files changed, 358 insertions(+), 88 deletions(-) create mode 100644 Hazel/Udp/UnityUdpClientConnection.cs diff --git a/Hazel/Connection.cs b/Hazel/Connection.cs index 2e68f73..f06a15f 100644 --- a/Hazel/Connection.cs +++ b/Hazel/Connection.cs @@ -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; - - /// - /// Reset event that is triggered when the connection is marked Connected. - /// - private ManualResetEvent connectWaitLock = new ManualResetEvent(false); - + protected virtual void SetState(ConnectionState state) { } + /// /// Constructor that initializes the ConnecitonStatistics object. /// @@ -225,15 +218,6 @@ namespace Hazel } } - /// - /// Blocks until the Connection is connected. - /// - /// The number of milliseconds to wait before timing out. - 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. @@ -259,7 +243,6 @@ namespace Hazel { this.DataReceived = null; this.Disconnected = null; - this.connectWaitLock.Dispose(); } } } diff --git a/Hazel/ConnectionStatistics.cs b/Hazel/ConnectionStatistics.cs index c183062..1e90105 100644 --- a/Hazel/ConnectionStatistics.cs +++ b/Hazel/ConnectionStatistics.cs @@ -25,7 +25,7 @@ namespace Hazel } /// - /// 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. /// /// /// This is the number of unreliable messages that were sent from the , incremented @@ -41,7 +41,7 @@ namespace Hazel } /// - /// The number of messages sent larger than 1400 bytes. + /// The number of messages sent larger than 576 bytes. /// 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); } diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index 5114784..0793714 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -87,6 +87,7 @@ + Code diff --git a/Hazel/NewConnectionEventArgs.cs b/Hazel/NewConnectionEventArgs.cs index 3c7ea9c..68fd37f 100644 --- a/Hazel/NewConnectionEventArgs.cs +++ b/Hazel/NewConnectionEventArgs.cs @@ -4,6 +4,7 @@ { /// /// The data received from the client in the handshake. + /// This data is yours. Remember to recycle it. /// public readonly MessageReader HandshakeData; diff --git a/Hazel/Udp/UdpBroadcastListener.cs b/Hazel/Udp/UdpBroadcastListener.cs index 973f63e..13b8d0b 100644 --- a/Hazel/Udp/UdpBroadcastListener.cs +++ b/Hazel/Udp/UdpBroadcastListener.cs @@ -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) diff --git a/Hazel/Udp/UdpClientConnection.cs b/Hazel/Udp/UdpClientConnection.cs index 503025d..1b3a22b 100644 --- a/Hazel/Udp/UdpClientConnection.cs +++ b/Hazel/Udp/UdpClientConnection.cs @@ -17,6 +17,11 @@ namespace Hazel.Udp /// private Socket socket; + /// + /// Reset event that is triggered when the connection is marked Connected. + /// + 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(); + } + + /// + /// Blocks until the Connection is connected. + /// + /// The number of milliseconds to wait before timing out. + public bool WaitOnConnect(int timeout) + { + return connectWaitLock.WaitOne(timeout); + } + /// /// Called when data has been received by the socket. /// @@ -316,6 +330,7 @@ namespace Hazel.Udp try { this.socket.Dispose(); } catch { } this.reliablePacketTimer.Dispose(); + this.connectWaitLock.Dispose(); base.Dispose(disposing); } diff --git a/Hazel/Udp/UdpConnection.cs b/Hazel/Udp/UdpConnection.cs index 07b66e7..083cb72 100644 --- a/Hazel/Udp/UdpConnection.cs +++ b/Hazel/Udp/UdpConnection.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Sockets; namespace Hazel.Udp { @@ -8,8 +9,43 @@ namespace Hazel.Udp /// 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; + } + /// /// Writes the given bytes to the connection. /// diff --git a/Hazel/Udp/UdpConnectionListener.cs b/Hazel/Udp/UdpConnectionListener.cs index 3d67fda..9bf15e1 100644 --- a/Hazel/Udp/UdpConnectionListener.cs +++ b/Hazel/Udp/UdpConnectionListener.cs @@ -12,25 +12,21 @@ namespace Hazel.Udp /// 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; /// - /// 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. /// - Socket socket; + public AcceptConnectionCheck AcceptConnection; + public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response); + private Socket socket; private Action Logger; + private Timer reliablePacketTimer; - Timer reliablePacketTimer; - - /// - /// The connections we currently hold - /// private ConcurrentDictionary allConnections = new ConcurrentDictionary(); 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 index 0000000..e067903 --- /dev/null +++ b/Hazel/Udp/UnityUdpClientConnection.cs @@ -0,0 +1,252 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + + +namespace Hazel.Udp +{ + /// + /// Represents a client's connection to a server that uses the UDP protocol. + /// + /// + public class UnityUdpClientConnection : UdpConnection + { + /// + /// The socket we're connected via. + /// + private Socket socket; + + /// + /// Creates a new UdpClientConnection. + /// + /// A to connect to. + 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(); + } + + /// + 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."); + } + + /// + 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(); + }); + } + + /// + /// Instructs the listener to begin listening. + /// + 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(); + } + } + + /// + /// Called when data has been received by the socket. + /// + /// The asyncronous operation's result. + 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); + } + + /// + /// Sends a disconnect message to the end point. + /// You may include optional disconnect data. The SendOption must be unreliable. + /// + 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; + } + + /// + 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 -- 2.39.5