using System; using System.Net; using System.Net.Sockets; using System.Threading; namespace Hazel.Udp { /// /// Unity doesn't always get along with thread pools well, so this interface will hopefully suit that case better. /// /// public class UnityUdpClientConnection : UdpConnection { /// /// The max size Hazel attempts to read from the network. /// Defaults to 8096. /// /// /// 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? /// 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() { } /// 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); } } /// /// Synchronously writes the given bytes to the connection. /// /// The bytes to write. 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(); } } /// 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; }); } /// /// Instructs the listener to begin listening. /// 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(); } } /// /// Called when data has been received by the socket. /// /// The asyncronous operation's result. 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); } /// /// 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 { this.WriteBytesToConnectionSync(bytes, bytes.Length); } 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); } } }