This commit is contained in:
2025-12-14 21:04:22 +01:00
parent c4eb7e944d
commit 3a7b81bfd7
112 changed files with 12 additions and 16665 deletions

View File

@@ -1,111 +0,0 @@
// 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.Linq;
using System.Net;
using System.Net.Sockets;
namespace Riptide.Transports.Udp
{
/// <summary>A client which can connect to a <see cref="UdpServer"/>.</summary>
public class UdpClient : UdpPeer, IClient
{
/// <inheritdoc/>
public event EventHandler Connected;
/// <inheritdoc/>
public event EventHandler ConnectionFailed;
/// <inheritdoc/>
public event EventHandler<DataReceivedEventArgs> DataReceived;
/// <summary>The connection to the server.</summary>
private UdpConnection udpConnection;
/// <inheritdoc/>
public UdpClient(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
/// <inheritdoc/>
/// <remarks>Expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>.</remarks>
public bool Connect(string hostAddress, out Connection connection, out string connectError)
{
connectError = $"Invalid host address '{hostAddress}'! IP and port should be separated by a colon, for example: '127.0.0.1:7777'.";
if (!ParseHostAddress(hostAddress, out IPAddress ip, out ushort port))
{
connection = null;
return false;
}
if ((mode == SocketMode.IPv4Only && ip.AddressFamily == AddressFamily.InterNetworkV6) || (mode == SocketMode.IPv6Only && ip.AddressFamily == AddressFamily.InterNetwork))
{
// The IP address isn't in an acceptable format for the current socket mode
if (mode == SocketMode.IPv4Only)
connectError = "Connecting to IPv6 addresses is not allowed when running in IPv4 only mode!";
else
connectError = "Connecting to IPv4 addresses is not allowed when running in IPv6 only mode!";
connection = null;
return false;
}
OpenSocket();
connection = udpConnection = new UdpConnection(new IPEndPoint(mode == SocketMode.IPv4Only ? ip : ip.MapToIPv6(), port), this);
OnConnected(); // UDP is connectionless, so from the transport POV everything is immediately ready to send/receive data
return true;
}
/// <summary>Parses <paramref name="hostAddress"/> into <paramref name="ip"/> and <paramref name="port"/>, if possible.</summary>
/// <param name="hostAddress">The host address to parse.</param>
/// <param name="ip">The retrieved IP.</param>
/// <param name="port">The retrieved port.</param>
/// <returns>Whether or not <paramref name="hostAddress"/> was in a valid format.</returns>
private bool ParseHostAddress(string hostAddress, out IPAddress ip, out ushort port)
{
string[] ipAndPort = hostAddress.Split(':');
string ipString = "";
string portString = "";
if (ipAndPort.Length > 2)
{
// There was more than one ':' in the host address, might be IPv6
ipString = string.Join(":", ipAndPort.Take(ipAndPort.Length - 1));
portString = ipAndPort[ipAndPort.Length - 1];
}
else if (ipAndPort.Length == 2)
{
// IPv4
ipString = ipAndPort[0];
portString = ipAndPort[1];
}
port = 0; // Need to make sure a value is assigned in case IP parsing fails
return IPAddress.TryParse(ipString, out ip) && ushort.TryParse(portString, out port);
}
/// <inheritdoc/>
public void Disconnect()
{
CloseSocket();
}
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
protected virtual void OnConnected()
{
Connected?.Invoke(this, EventArgs.Empty);
}
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
protected virtual void OnConnectionFailed()
{
ConnectionFailed?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
{
if (udpConnection.RemoteEndPoint.Equals(fromEndPoint) && !udpConnection.IsNotConnected)
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, udpConnection));
}
}
}

View File

@@ -1,80 +0,0 @@
// 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 Riptide.Utils;
using System;
using System.Collections.Generic;
using System.Net;
namespace Riptide.Transports.Udp
{
/// <summary>Represents a connection to a <see cref="UdpServer"/> or <see cref="UdpClient"/>.</summary>
public class UdpConnection : Connection, IEquatable<UdpConnection>
{
/// <summary>The endpoint representing the other end of the connection.</summary>
public readonly IPEndPoint RemoteEndPoint;
/// <summary>The local peer this connection is associated with.</summary>
private readonly UdpPeer peer;
/// <summary>Initializes the connection.</summary>
/// <param name="remoteEndPoint">The endpoint representing the other end of the connection.</param>
/// <param name="peer">The local peer this connection is associated with.</param>
internal UdpConnection(IPEndPoint remoteEndPoint, UdpPeer peer)
{
RemoteEndPoint = remoteEndPoint;
this.peer = peer;
}
/// <inheritdoc/>
protected internal override void Send(byte[] dataBuffer, int amount)
{
peer.Send(dataBuffer, amount, RemoteEndPoint);
}
/// <inheritdoc/>
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
/// <inheritdoc/>
public override bool Equals(object obj) => Equals(obj as UdpConnection);
/// <inheritdoc/>
public bool Equals(UdpConnection other)
{
if (other is null)
return false;
if (ReferenceEquals(this, other))
return true;
return RemoteEndPoint.Equals(other.RemoteEndPoint);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return -288961498 + EqualityComparer<IPEndPoint>.Default.GetHashCode(RemoteEndPoint);
}
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static bool operator ==(UdpConnection left, UdpConnection right)
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
if (left is null)
{
if (right is null)
return true;
return false; // Only the left side is null
}
// Equals handles case of null on right side
return left.Equals(right);
}
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static bool operator !=(UdpConnection left, UdpConnection right) => !(left == right);
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
}

View File

@@ -1,185 +0,0 @@
// 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
{
/// <summary>The kind of socket to create.</summary>
public enum SocketMode
{
/// <summary>Dual-mode. Works with both IPv4 and IPv6.</summary>
Both,
/// <summary>IPv4 only mode.</summary>
IPv4Only,
/// <summary>IPv6 only mode.</summary>
IPv6Only
}
/// <summary>Provides base send &#38; receive functionality for <see cref="UdpServer"/> and <see cref="UdpClient"/>.</summary>
public abstract class UdpPeer
{
/// <inheritdoc cref="IPeer.Disconnected"/>
public event EventHandler<DisconnectedEventArgs> Disconnected;
/// <summary>The default size used for the socket's send and receive buffers.</summary>
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
/// <summary>The minimum size that may be used for the socket's send and receive buffers.</summary>
private const int MinSocketBufferSize = 256 * 1024; // 256KB
/// <summary>How long to wait for a packet, in microseconds.</summary>
private const int ReceivePollingTime = 500000; // 0.5 seconds
/// <summary>Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</summary>
protected readonly SocketMode mode;
/// <summary>The size to use for the socket's send and receive buffers.</summary>
private readonly int socketBufferSize;
/// <summary>The array that incoming data is received into.</summary>
private readonly byte[] receivedData;
/// <summary>The socket to use for sending and receiving.</summary>
private Socket socket;
/// <summary>Whether or not the transport is running.</summary>
private bool isRunning;
/// <summary>A reusable endpoint.</summary>
private EndPoint remoteEndPoint;
/// <summary>Initializes the transport.</summary>
/// <param name="mode">Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</param>
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
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];
}
/// <inheritdoc cref="IPeer.Poll"/>
public void Poll()
{
Receive();
}
/// <summary>Opens the socket and starts the transport.</summary>
/// <param name="listenAddress">The IP address to bind the socket to, if any.</param>
/// <param name="port">The port to bind the socket to.</param>
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;
}
/// <summary>Closes the socket and stops the transport.</summary>
protected void CloseSocket()
{
if (!isRunning)
return;
isRunning = false;
socket.Close();
}
/// <summary>Polls the socket and checks if any data was received.</summary>
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);
}
}
/// <summary>Sends data to a given endpoint.</summary>
/// <param name="dataBuffer">The array containing the data.</param>
/// <param name="numBytes">The number of bytes in the array which should be sent.</param>
/// <param name="toEndPoint">The endpoint to send the data to.</param>
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...
}
}
/// <summary>Handles received data.</summary>
/// <param name="dataBuffer">A byte array containing the received data.</param>
/// <param name="amount">The number of bytes in <paramref name="dataBuffer"/> used by the received data.</param>
/// <param name="fromEndPoint">The endpoint from which the data was received.</param>
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint);
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
/// <param name="connection">The closed connection.</param>
/// <param name="reason">The reason for the disconnection.</param>
protected virtual void OnDisconnected(Connection connection, DisconnectReason reason)
{
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
}
}
}

View File

@@ -1,93 +0,0 @@
// 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.Collections.Generic;
using System.Net;
namespace Riptide.Transports.Udp
{
/// <summary>A server which can accept connections from <see cref="UdpClient"/>s.</summary>
public class UdpServer : UdpPeer, IServer
{
/// <inheritdoc/>
public event EventHandler<ConnectedEventArgs> Connected;
/// <inheritdoc/>
public event EventHandler<DataReceivedEventArgs> DataReceived;
/// <inheritdoc/>
public ushort Port { get; private set; }
/// <summary>The currently open connections, accessible by their endpoints.</summary>
private Dictionary<IPEndPoint, Connection> connections;
/// <summary>The IP address to bind the socket to, if any.</summary>
private readonly IPAddress listenAddress;
/// <inheritdoc/>
public UdpServer(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
/// <summary>Initializes the transport, binding the socket to a specific IP address.</summary>
/// <param name="listenAddress">The IP address to bind the socket to.</param>
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
public UdpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(SocketMode.Both, socketBufferSize)
{
this.listenAddress = listenAddress;
}
/// <inheritdoc/>
public void Start(ushort port)
{
Port = port;
connections = new Dictionary<IPEndPoint, Connection>();
OpenSocket(listenAddress, port);
}
/// <summary>Decides what to do with a connection attempt.</summary>
/// <param name="fromEndPoint">The endpoint the connection attempt is coming from.</param>
/// <returns>Whether or not the connection attempt was from a new connection.</returns>
private bool HandleConnectionAttempt(IPEndPoint fromEndPoint)
{
if (connections.ContainsKey(fromEndPoint))
return false;
UdpConnection connection = new UdpConnection(fromEndPoint, this);
connections.Add(fromEndPoint, connection);
OnConnected(connection);
return true;
}
/// <inheritdoc/>
public void Close(Connection connection)
{
if (connection is UdpConnection udpConnection)
connections.Remove(udpConnection.RemoteEndPoint);
}
/// <inheritdoc/>
public void Shutdown()
{
CloseSocket();
connections.Clear();
}
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
/// <param name="connection">The successfully established connection.</param>
protected virtual void OnConnected(Connection connection)
{
Connected?.Invoke(this, new ConnectedEventArgs(connection));
}
/// <inheritdoc/>
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
{
if ((MessageHeader)(dataBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect && !HandleConnectionAttempt(fromEndPoint))
return;
if (connections.TryGetValue(fromEndPoint, out Connection connection) && !connection.IsNotConnected)
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, connection));
}
}
}