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 sealed class UdpClientConnection : 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; /// /// The socket we're connected via. /// 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 public event Action DataSentRaw; public event Action DataReceivedRaw; #endif /// /// Creates a new UdpClientConnection. /// /// A to connect to. 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 { } } /// 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); } } /// 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."); } } /// 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(); }); } /// /// Instructs the listener to begin listening. /// void StartListeningForData() { #if DEBUG if (this.TestLagMs > 0) { Thread.Sleep(this.TestLagMs); } #endif var msg = MessageReader.GetSized(this.ReceiveBufferSize);//一个父message try { // Buffer包含MessageWriter的整个内容,包括header 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) { } } /// /// 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. /// /// 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(); 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(); //继续接受消息。它这里没有用async await在一个while里轮询,所以需要嵌套调用 } 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 //c //! 重点看这里面长什么样 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; // 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; } /// 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); } } }