using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace NetCoreServer
{
///
/// UDP client is used to read/write data from/into the connected UDP server
///
/// Thread-safe
public class UdpClient : IDisposable
{
///
/// Initialize UDP client with a given server IP address and port number
///
/// IP address
/// Port number
public UdpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) {}
///
/// Initialize UDP client with a given server IP address and port number
///
/// IP address
/// Port number
public UdpClient(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {}
///
/// Initialize UDP client with a given DNS endpoint
///
/// DNS endpoint
public UdpClient(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {}
///
/// Initialize UDP client with a given IP endpoint
///
/// IP endpoint
public UdpClient(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {}
///
/// Initialize UDP client with a given endpoint, address and port
///
/// Endpoint
/// Server address
/// Server port
private UdpClient(EndPoint endpoint, string address, int port)
{
Id = Guid.NewGuid();
Address = address;
Port = port;
Endpoint = endpoint;
}
///
/// Client Id
///
public Guid Id { get; }
///
/// UDP server address
///
public string Address { get; }
///
/// UDP server port
///
public int Port { get; }
///
/// Endpoint
///
public EndPoint Endpoint { get; private set; }
///
/// Socket
///
public Socket Socket { get; private set; }
///
/// Number of bytes pending sent by the client
///
public long BytesPending { get; private set; }
///
/// Number of bytes sending by the client
///
public long BytesSending { get; private set; }
///
/// Number of bytes sent by the client
///
public long BytesSent { get; private set; }
///
/// Number of bytes received by the client
///
public long BytesReceived { get; private set; }
///
/// Number of datagrams sent by the client
///
public long DatagramsSent { get; private set; }
///
/// Number of datagrams received by the client
///
public long DatagramsReceived { get; private set; }
///
/// Option: dual mode socket
///
///
/// Specifies whether the Socket is a dual-mode socket used for both IPv4 and IPv6.
/// Will work only if socket is bound on IPv6 address.
///
public bool OptionDualMode { get; set; }
///
/// Option: reuse address
///
///
/// This option will enable/disable SO_REUSEADDR if the OS support this feature
///
public bool OptionReuseAddress { get; set; }
///
/// Option: enables a socket to be bound for exclusive access
///
///
/// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature
///
public bool OptionExclusiveAddressUse { get; set; }
///
/// Option: bind the socket to the multicast UDP server
///
public bool OptionMulticast { get; set; }
///
/// Option: receive buffer limit
///
public int OptionReceiveBufferLimit { get; set; } = 0;
///
/// Option: receive buffer size
///
public int OptionReceiveBufferSize { get; set; } = 8192;
///
/// Option: send buffer limit
///
public int OptionSendBufferLimit { get; set; } = 0;
///
/// Option: send buffer size
///
public int OptionSendBufferSize { get; set; } = 8192;
#region Connect/Disconnect client
///
/// Is the client connected?
///
public bool IsConnected { get; private set; }
///
/// Create a new socket object
///
///
/// Method may be override if you need to prepare some specific socket object in your implementation.
///
/// Socket object
protected virtual Socket CreateSocket()
{
return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
}
///
/// Connect the client (synchronous)
///
/// 'true' if the client was successfully connected, 'false' if the client failed to connect
public virtual bool Connect()
{
if (IsConnected)
return false;
// Setup buffers
_receiveBuffer = new Buffer();
_sendBuffer = new Buffer();
// Setup event args
_receiveEventArg = new SocketAsyncEventArgs();
_receiveEventArg.Completed += OnAsyncCompleted;
_sendEventArg = new SocketAsyncEventArgs();
_sendEventArg.Completed += OnAsyncCompleted;
// Create a new client socket
Socket = CreateSocket();
// Update the client socket disposed flag
IsSocketDisposed = false;
// Apply the option: reuse address
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress);
// Apply the option: exclusive address use
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse);
// Apply the option: dual mode (this option must be applied before recieving/sending)
if (Socket.AddressFamily == AddressFamily.InterNetworkV6)
Socket.DualMode = OptionDualMode;
// Call the client connecting handler
OnConnecting();
try
{
// Bind the acceptor socket to the endpoint
if (OptionMulticast)
Socket.Bind(Endpoint);
else
{
var endpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0);
Socket.Bind(endpoint);
}
}
catch (SocketException ex)
{
// Call the client error handler
SendError(ex.SocketErrorCode);
// Reset event args
_receiveEventArg.Completed -= OnAsyncCompleted;
_sendEventArg.Completed -= OnAsyncCompleted;
// Call the client disconnecting handler
OnDisconnecting();
// Close the client socket
Socket.Close();
// Dispose the client socket
Socket.Dispose();
// Dispose event arguments
_receiveEventArg.Dispose();
_sendEventArg.Dispose();
// Call the client disconnected handler
OnDisconnected();
return false;
}
// Prepare receive endpoint
_receiveEndpoint = new IPEndPoint((Endpoint.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0);
// Prepare receive & send buffers
_receiveBuffer.Reserve(OptionReceiveBufferSize);
// Reset statistic
BytesPending = 0;
BytesSending = 0;
BytesSent = 0;
BytesReceived = 0;
DatagramsSent = 0;
DatagramsReceived = 0;
// Update the connected flag
IsConnected = true;
// Call the client connected handler
OnConnected();
return true;
}
///
/// Disconnect the client (synchronous)
///
/// 'true' if the client was successfully disconnected, 'false' if the client is already disconnected
public virtual bool Disconnect()
{
if (!IsConnected)
return false;
// Reset event args
_receiveEventArg.Completed -= OnAsyncCompleted;
_sendEventArg.Completed -= OnAsyncCompleted;
// Call the client disconnecting handler
OnDisconnecting();
try
{
// Close the client socket
Socket.Close();
// Dispose the client socket
Socket.Dispose();
// Dispose event arguments
_receiveEventArg.Dispose();
_sendEventArg.Dispose();
// Update the client socket disposed flag
IsSocketDisposed = true;
}
catch (ObjectDisposedException) {}
// Update the connected flag
IsConnected = false;
// Update sending/receiving flags
_receiving = false;
_sending = false;
// Clear send/receive buffers
ClearBuffers();
// Call the client disconnected handler
OnDisconnected();
return true;
}
///
/// Reconnect the client (synchronous)
///
/// 'true' if the client was successfully reconnected, 'false' if the client is already reconnected
public virtual bool Reconnect()
{
if (!Disconnect())
return false;
return Connect();
}
#endregion
#region Multicast group
///
/// Setup multicast: bind the socket to the multicast UDP server
///
/// Enable/disable multicast
public virtual void SetupMulticast(bool enable)
{
OptionReuseAddress = enable;
OptionMulticast = enable;
}
///
/// Join multicast group with a given IP address (synchronous)
///
/// IP address
public virtual void JoinMulticastGroup(IPAddress address)
{
if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6)
Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddMembership, new IPv6MulticastOption(address));
else
Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(address));
// Call the client joined multicast group notification
OnJoinedMulticastGroup(address);
}
///
/// Join multicast group with a given IP address (synchronous)
///
/// IP address
public virtual void JoinMulticastGroup(string address) { JoinMulticastGroup(IPAddress.Parse(address)); }
///
/// Leave multicast group with a given IP address (synchronous)
///
/// IP address
public virtual void LeaveMulticastGroup(IPAddress address)
{
if (Endpoint.AddressFamily == AddressFamily.InterNetworkV6)
Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.DropMembership, new IPv6MulticastOption(address));
else
Socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, new MulticastOption(address));
// Call the client left multicast group notification
OnLeftMulticastGroup(address);
}
///
/// Leave multicast group with a given IP address (synchronous)
///
/// IP address
public virtual void LeaveMulticastGroup(string address) { LeaveMulticastGroup(IPAddress.Parse(address)); }
#endregion
#region Send/Receive data
// Receive and send endpoints
EndPoint _receiveEndpoint;
EndPoint _sendEndpoint;
// Receive buffer
private bool _receiving;
private Buffer _receiveBuffer;
private SocketAsyncEventArgs _receiveEventArg;
// Send buffer
private bool _sending;
private Buffer _sendBuffer;
private SocketAsyncEventArgs _sendEventArg;
///
/// Send datagram to the connected server (synchronous)
///
/// Datagram buffer to send
/// Size of sent datagram
public virtual long Send(byte[] buffer) => Send(buffer.AsSpan());
///
/// Send datagram to the connected server (synchronous)
///
/// Datagram buffer to send
/// Datagram buffer offset
/// Datagram buffer size
/// Size of sent datagram
public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size));
///
/// Send datagram to the connected server (synchronous)
///
/// Datagram buffer to send as a span of bytes
/// Size of sent datagram
public virtual long Send(ReadOnlySpan buffer) => Send(Endpoint, buffer);
///
/// Send text to the connected server (synchronous)
///
/// Text string to send
/// Size of sent datagram
public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text));
///
/// Send text to the connected server (synchronous)
///
/// Text to send as a span of characters
/// Size of sent datagram
public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Send datagram to the given endpoint (synchronous)
///
/// Endpoint to send
/// Datagram buffer to send
/// Size of sent datagram
public virtual long Send(EndPoint endpoint, byte[] buffer) => Send(endpoint, buffer.AsSpan());
///
/// Send datagram to the given endpoint (synchronous)
///
/// Endpoint to send
/// Datagram buffer to send
/// Datagram buffer offset
/// Datagram buffer size
/// Size of sent datagram
public virtual long Send(EndPoint endpoint, byte[] buffer, long offset, long size) => Send(endpoint, buffer.AsSpan((int)offset, (int)size));
///
/// Send datagram to the given endpoint (synchronous)
///
/// Endpoint to send
/// Datagram buffer to send as a span of bytes
/// Size of sent datagram
public virtual long Send(EndPoint endpoint, ReadOnlySpan buffer)
{
if (!IsConnected)
return 0;
if (buffer.IsEmpty)
return 0;
try
{
// Sent datagram to the server
long sent = Socket.SendTo(buffer, SocketFlags.None, endpoint);
if (sent > 0)
{
// Update statistic
DatagramsSent++;
BytesSent += sent;
// Call the datagram sent handler
OnSent(endpoint, sent);
}
return sent;
}
catch (ObjectDisposedException) { return 0; }
catch (SocketException ex)
{
SendError(ex.SocketErrorCode);
Disconnect();
return 0;
}
}
///
/// Send text to the given endpoint (synchronous)
///
/// Endpoint to send
/// Text string to send
/// Size of sent datagram
public virtual long Send(EndPoint endpoint, string text) => Send(endpoint, Encoding.UTF8.GetBytes(text));
///
/// Send text to the given endpoint (synchronous)
///
/// Endpoint to send
/// Text to send as a span of characters
/// Size of sent datagram
public virtual long Send(EndPoint endpoint, ReadOnlySpan text) => Send(endpoint, Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Send datagram to the connected server (asynchronous)
///
/// Datagram buffer to send
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan());
///
/// Send datagram to the connected server (asynchronous)
///
/// Datagram buffer to send
/// Datagram buffer offset
/// Datagram buffer size
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size));
///
/// Send datagram to the connected server (asynchronous)
///
/// Datagram buffer to send as a span of bytes
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(ReadOnlySpan buffer) => SendAsync(Endpoint, buffer);
///
/// Send text to the connected server (asynchronous)
///
/// Text string to send
/// 'true' if the text was successfully sent, 'false' if the text was not sent
public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text));
///
/// Send text to the connected server (asynchronous)
///
/// Text to send as a span of characters
/// 'true' if the text was successfully sent, 'false' if the text was not sent
public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Send datagram to the given endpoint (asynchronous)
///
/// Endpoint to send
/// Datagram buffer to send
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(EndPoint endpoint, byte[] buffer) => SendAsync(endpoint, buffer.AsSpan());
///
/// Send datagram to the given endpoint (asynchronous)
///
/// Endpoint to send
/// Datagram buffer to send
/// Datagram buffer offset
/// Datagram buffer size
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(EndPoint endpoint, byte[] buffer, long offset, long size) => SendAsync(endpoint, buffer.AsSpan((int)offset, (int)size));
///
/// Send datagram to the given endpoint (asynchronous)
///
/// Endpoint to send
/// Datagram buffer to send as a span of bytes
/// 'true' if the datagram was successfully sent, 'false' if the datagram was not sent
public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan buffer)
{
if (_sending)
return false;
if (!IsConnected)
return false;
if (buffer.IsEmpty)
return true;
// Check the send buffer limit
if (((_sendBuffer.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
return false;
}
// Fill the main send buffer
_sendBuffer.Append(buffer);
// Update statistic
BytesSending = _sendBuffer.Size;
// Update send endpoint
_sendEndpoint = endpoint;
// Try to send the main buffer
TrySend();
return true;
}
///
/// Send text to the given endpoint (asynchronous)
///
/// Endpoint to send
/// Text string to send
/// 'true' if the text was successfully sent, 'false' if the text was not sent
public virtual bool SendAsync(EndPoint endpoint, string text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text));
///
/// Send text to the given endpoint (asynchronous)
///
/// Endpoint to send
/// Text to send as a span of characters
/// 'true' if the text was successfully sent, 'false' if the text was not sent
public virtual bool SendAsync(EndPoint endpoint, ReadOnlySpan text) => SendAsync(endpoint, Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Receive a new datagram from the given endpoint (synchronous)
///
/// Endpoint to receive from
/// Datagram buffer to receive
/// Size of received datagram
public virtual long Receive(ref EndPoint endpoint, byte[] buffer) { return Receive(ref endpoint, buffer, 0, buffer.Length); }
///
/// Receive a new datagram from the given endpoint (synchronous)
///
/// Endpoint to receive from
/// Datagram buffer to receive
/// Datagram buffer offset
/// Datagram buffer size
/// Size of received datagram
public virtual long Receive(ref EndPoint endpoint, byte[] buffer, long offset, long size)
{
if (!IsConnected)
return 0;
if (size == 0)
return 0;
try
{
// Receive datagram from the server
long received = Socket.ReceiveFrom(buffer, (int)offset, (int)size, SocketFlags.None, ref endpoint);
// Update statistic
DatagramsReceived++;
BytesReceived += received;
// Call the datagram received handler
OnReceived(endpoint, buffer, offset, size);
return received;
}
catch (ObjectDisposedException) { return 0; }
catch (SocketException ex)
{
SendError(ex.SocketErrorCode);
Disconnect();
return 0;
}
}
///
/// Receive text from the given endpoint (synchronous)
///
/// Endpoint to receive from
/// Text size to receive
/// Received text
public virtual string Receive(ref EndPoint endpoint, long size)
{
var buffer = new byte[size];
var length = Receive(ref endpoint, buffer);
return Encoding.UTF8.GetString(buffer, 0, (int)length);
}
///
/// Receive datagram from the server (asynchronous)
///
public virtual void ReceiveAsync()
{
// Try to receive datagram
TryReceive();
}
///
/// Try to receive new data
///
private void TryReceive()
{
if (_receiving)
return;
if (!IsConnected)
return;
try
{
// Async receive with the receive handler
_receiving = true;
_receiveEventArg.RemoteEndPoint = _receiveEndpoint;
_receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity);
if (!Socket.ReceiveFromAsync(_receiveEventArg))
ProcessReceiveFrom(_receiveEventArg);
}
catch (ObjectDisposedException) {}
}
///
/// Try to send pending data
///
private void TrySend()
{
if (_sending)
return;
if (!IsConnected)
return;
try
{
// Async write with the write handler
_sending = true;
_sendEventArg.RemoteEndPoint = _sendEndpoint;
_sendEventArg.SetBuffer(_sendBuffer.Data, 0, (int)(_sendBuffer.Size));
if (!Socket.SendToAsync(_sendEventArg))
ProcessSendTo(_sendEventArg);
}
catch (ObjectDisposedException) {}
}
///
/// Clear send/receive buffers
///
private void ClearBuffers()
{
// Clear send buffers
_sendBuffer.Clear();
// Update statistic
BytesPending = 0;
BytesSending = 0;
}
#endregion
#region IO processing
///
/// This method is called whenever a receive or send operation is completed on a socket
///
private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
{
if (IsSocketDisposed)
return;
// Determine which type of operation just completed and call the associated handler
switch (e.LastOperation)
{
case SocketAsyncOperation.ReceiveFrom:
ProcessReceiveFrom(e);
break;
case SocketAsyncOperation.SendTo:
ProcessSendTo(e);
break;
default:
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
}
}
///
/// This method is invoked when an asynchronous receive from operation completes
///
private void ProcessReceiveFrom(SocketAsyncEventArgs e)
{
_receiving = false;
if (!IsConnected)
return;
// Disconnect on error
if (e.SocketError != SocketError.Success)
{
SendError(e.SocketError);
Disconnect();
return;
}
// Received some data from the server
long size = e.BytesTransferred;
// Update statistic
DatagramsReceived++;
BytesReceived += size;
// Call the datagram received handler
OnReceived(e.RemoteEndPoint, _receiveBuffer.Data, 0, size);
// If the receive buffer is full increase its size
if (_receiveBuffer.Capacity == size)
{
// Check the receive buffer limit
if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
Disconnect();
return;
}
_receiveBuffer.Reserve(2 * size);
}
}
///
/// This method is invoked when an asynchronous send to operation completes
///
private void ProcessSendTo(SocketAsyncEventArgs e)
{
_sending = false;
if (!IsConnected)
return;
// Disconnect on error
if (e.SocketError != SocketError.Success)
{
SendError(e.SocketError);
Disconnect();
return;
}
long sent = e.BytesTransferred;
// Send some data to the server
if (sent > 0)
{
// Update statistic
BytesSending = 0;
BytesSent += sent;
// Clear the send buffer
_sendBuffer.Clear();
// Call the buffer sent handler
OnSent(_sendEndpoint, sent);
}
}
#endregion
#region Session handlers
///
/// Handle client connecting notification
///
protected virtual void OnConnecting() {}
///
/// Handle client connected notification
///
protected virtual void OnConnected() {}
///
/// Handle client disconnecting notification
///
protected virtual void OnDisconnecting() {}
///
/// Handle client disconnected notification
///
protected virtual void OnDisconnected() {}
///
/// Handle client joined multicast group notification
///
/// IP address
protected virtual void OnJoinedMulticastGroup(IPAddress address) {}
///
/// Handle client left multicast group notification
///
/// IP address
protected virtual void OnLeftMulticastGroup(IPAddress address) {}
///
/// Handle datagram received notification
///
/// Received endpoint
/// Received datagram buffer
/// Received datagram buffer offset
/// Received datagram buffer size
///
/// Notification is called when another datagram was received from some endpoint
///
protected virtual void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) {}
///
/// Handle datagram sent notification
///
/// Endpoint of sent datagram
/// Size of sent datagram buffer
///
/// Notification is called when a datagram was sent to the server.
/// This handler could be used to send another datagram to the server for instance when the pending size is zero.
///
protected virtual void OnSent(EndPoint endpoint, long sent) {}
///
/// Handle error notification
///
/// Socket error code
protected virtual void OnError(SocketError error) {}
#endregion
#region Error handling
///
/// Send error notification
///
/// Socket error code
private void SendError(SocketError error)
{
// Skip disconnect errors
if ((error == SocketError.ConnectionAborted) ||
(error == SocketError.ConnectionRefused) ||
(error == SocketError.ConnectionReset) ||
(error == SocketError.OperationAborted) ||
(error == SocketError.Shutdown))
return;
OnError(error);
}
#endregion
#region IDisposable implementation
///
/// Disposed flag
///
public bool IsDisposed { get; private set; }
///
/// Client socket disposed flag
///
public bool IsSocketDisposed { get; private set; } = true;
// Implement IDisposable.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposingManagedResources)
{
// The idea here is that Dispose(Boolean) knows whether it is
// being called to do explicit cleanup (the Boolean is true)
// versus being called due to a garbage collection (the Boolean
// is false). This distinction is useful because, when being
// disposed explicitly, the Dispose(Boolean) method can safely
// execute code using reference type fields that refer to other
// objects knowing for sure that these other objects have not been
// finalized or disposed of yet. When the Boolean is false,
// the Dispose(Boolean) method should not execute code that
// refer to reference type fields because those objects may
// have already been finalized."
if (!IsDisposed)
{
if (disposingManagedResources)
{
// Dispose managed resources here...
Disconnect();
}
// Dispose unmanaged resources here...
// Set large fields to null here...
// Mark as disposed.
IsDisposed = true;
}
}
#endregion
}
}