// This file is provided under The MIT License as part of RiptideNetworking.
// Copyright (c) Tom Weiland
// For additional information please see the included LICENSE.md file or view it on GitHub:
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
using System;
using System.Net;
using System.Net.Sockets;
namespace Riptide.Transports.Udp
{
/// The kind of socket to create.
public enum SocketMode
{
/// Dual-mode. Works with both IPv4 and IPv6.
Both,
/// IPv4 only mode.
IPv4Only,
/// IPv6 only mode.
IPv6Only
}
/// Provides base send & receive functionality for and .
public abstract class UdpPeer
{
///
public event EventHandler Disconnected;
/// The default size used for the socket's send and receive buffers.
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
/// The minimum size that may be used for the socket's send and receive buffers.
private const int MinSocketBufferSize = 256 * 1024; // 256KB
/// How long to wait for a packet, in microseconds.
private const int ReceivePollingTime = 500000; // 0.5 seconds
/// Whether to create an IPv4 only, IPv6 only, or dual-mode socket.
protected readonly SocketMode mode;
/// The size to use for the socket's send and receive buffers.
private readonly int socketBufferSize;
/// The array that incoming data is received into.
private readonly byte[] receivedData;
/// The socket to use for sending and receiving.
private Socket socket;
/// Whether or not the transport is running.
private bool isRunning;
/// A reusable endpoint.
private EndPoint remoteEndPoint;
/// Initializes the transport.
/// Whether to create an IPv4 only, IPv6 only, or dual-mode socket.
/// How big the socket's send and receive buffers should be.
protected UdpPeer(SocketMode mode, int socketBufferSize)
{
if (socketBufferSize < MinSocketBufferSize)
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
this.mode = mode;
this.socketBufferSize = socketBufferSize;
receivedData = new byte[Message.MaxSize];
}
///
public void Poll()
{
Receive();
}
/// Opens the socket and starts the transport.
/// The IP address to bind the socket to, if any.
/// The port to bind the socket to.
protected void OpenSocket(IPAddress listenAddress = null, ushort port = 0)
{
if (isRunning)
CloseSocket();
if (mode == SocketMode.IPv4Only)
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
else if (mode == SocketMode.IPv6Only)
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) { DualMode = false };
else
socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
IPAddress any = socket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any;
socket.SendBufferSize = socketBufferSize;
socket.ReceiveBufferSize = socketBufferSize;
socket.Bind(new IPEndPoint(listenAddress == null ? any : listenAddress, port));
remoteEndPoint = new IPEndPoint(any, 0);
isRunning = true;
}
/// Closes the socket and stops the transport.
protected void CloseSocket()
{
if (!isRunning)
return;
isRunning = false;
socket.Close();
}
/// Polls the socket and checks if any data was received.
private void Receive()
{
if (!isRunning)
return;
bool tryReceiveMore = true;
while (tryReceiveMore)
{
int byteCount = 0;
try
{
if (socket.Available > 0 && socket.Poll(ReceivePollingTime, SelectMode.SelectRead))
byteCount = socket.ReceiveFrom(receivedData, SocketFlags.None, ref remoteEndPoint);
else
tryReceiveMore = false;
}
catch (SocketException ex)
{
tryReceiveMore = false;
switch (ex.SocketErrorCode)
{
case SocketError.Interrupted:
case SocketError.NotSocket:
isRunning = false;
break;
case SocketError.ConnectionReset:
case SocketError.MessageSize:
case SocketError.TimedOut:
break;
default:
break;
}
}
catch (ObjectDisposedException)
{
tryReceiveMore = false;
isRunning = false;
}
catch (NullReferenceException)
{
tryReceiveMore = false;
isRunning = false;
}
if (byteCount > 0)
OnDataReceived(receivedData, byteCount, (IPEndPoint)remoteEndPoint);
}
}
/// Sends data to a given endpoint.
/// The array containing the data.
/// The number of bytes in the array which should be sent.
/// The endpoint to send the data to.
internal void Send(byte[] dataBuffer, int numBytes, IPEndPoint toEndPoint)
{
try
{
if (isRunning)
socket.SendTo(dataBuffer, numBytes, SocketFlags.None, toEndPoint);
}
catch(SocketException)
{
// May want to consider triggering a disconnect here (perhaps depending on the type
// of SocketException)? Timeout should catch disconnections, but disconnecting
// explicitly might be better...
}
}
/// Handles received data.
/// A byte array containing the received data.
/// The number of bytes in used by the received data.
/// The endpoint from which the data was received.
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint);
/// Invokes the event.
/// The closed connection.
/// The reason for the disconnection.
protected virtual void OnDisconnected(Connection connection, DisconnectReason reason)
{
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
}
}
}