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);
}
}
}