first commit

This commit is contained in:
2025-12-13 14:28:35 +01:00
commit 679c3c9a52
113 changed files with 715750 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
// 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.Tcp
{
/// <summary>A client which can connect to a <see cref="TcpServer"/>.</summary>
public class TcpClient : TcpPeer, 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 TcpConnection tcpConnection;
/// <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;
}
IPEndPoint remoteEndPoint = new IPEndPoint(ip, port);
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
{
SendBufferSize = socketBufferSize,
ReceiveBufferSize = socketBufferSize,
NoDelay = true,
};
try
{
socket.Connect(remoteEndPoint); // TODO: do something about the fact that this is a blocking call
}
catch (SocketException)
{
// The connection failed, but invoking the transports ConnectionFailed event from
// inside this method will cause problems, so we're just goint to eat the exception,
// call OnConnected(), and let Riptide detect that no connection was established.
}
connection = tcpConnection = new TcpConnection(socket, remoteEndPoint, this);
OnConnected();
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 Poll()
{
if (tcpConnection != null)
tcpConnection.Receive();
}
/// <inheritdoc/>
public void Disconnect()
{
socket.Close();
tcpConnection = null;
}
/// <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 internal override void OnDataReceived(int amount, TcpConnection fromConnection)
{
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
}
}
}

View File

@@ -0,0 +1,195 @@
// 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;
using System.Net.Sockets;
namespace Riptide.Transports.Tcp
{
/// <summary>Represents a connection to a <see cref="TcpServer"/> or <see cref="TcpClient"/>.</summary>
public class TcpConnection : Connection, IEquatable<TcpConnection>
{
/// <summary>The endpoint representing the other end of the connection.</summary>
public readonly IPEndPoint RemoteEndPoint;
/// <summary>Whether or not the server has received a connection attempt from this connection.</summary>
internal bool DidReceiveConnect;
/// <summary>The socket to use for sending and receiving.</summary>
private readonly Socket socket;
/// <summary>The local peer this connection is associated with.</summary>
private readonly TcpPeer peer;
/// <summary>An array to receive message size values into.</summary>
private readonly byte[] sizeBytes = new byte[sizeof(int)];
/// <summary>The size of the next message to be received.</summary>
private int nextMessageSize;
/// <summary>Initializes the connection.</summary>
/// <param name="socket">The socket to use for sending and receiving.</param>
/// <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 TcpConnection(Socket socket, IPEndPoint remoteEndPoint, TcpPeer peer)
{
RemoteEndPoint = remoteEndPoint;
this.socket = socket;
this.peer = peer;
}
/// <inheritdoc/>
protected internal override void Send(byte[] dataBuffer, int amount)
{
if (amount == 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Sending 0 bytes is not allowed!");
try
{
if (socket.Connected)
{
Converter.FromInt(amount, peer.SendBuffer, 0);
Array.Copy(dataBuffer, 0, peer.SendBuffer, sizeof(int), amount); // TODO: consider sending length separately with an extra socket.Send call instead of copying the data an extra time
socket.Send(peer.SendBuffer, amount + sizeof(int), SocketFlags.None);
}
}
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>Polls the socket and checks if any data was received.</summary>
internal void Receive()
{
bool tryReceiveMore = true;
while (tryReceiveMore)
{
int byteCount = 0;
try
{
if (nextMessageSize > 0)
{
// We already have a size value
tryReceiveMore = TryReceiveMessage(out byteCount);
}
else if (socket.Available >= sizeof(int))
{
// We have enough bytes for a complete size value
socket.Receive(sizeBytes, sizeof(int), SocketFlags.None);
nextMessageSize = Converter.ToInt(sizeBytes, 0);
if (nextMessageSize > 0)
tryReceiveMore = TryReceiveMessage(out byteCount);
}
else
tryReceiveMore = false;
}
catch (SocketException ex)
{
tryReceiveMore = false;
switch (ex.SocketErrorCode)
{
case SocketError.Interrupted:
case SocketError.NotSocket:
peer.OnDisconnected(this, DisconnectReason.TransportError);
break;
case SocketError.ConnectionReset:
peer.OnDisconnected(this, DisconnectReason.Disconnected);
break;
case SocketError.TimedOut:
peer.OnDisconnected(this, DisconnectReason.TimedOut);
break;
case SocketError.MessageSize:
break;
default:
break;
}
}
catch (ObjectDisposedException)
{
tryReceiveMore = false;
peer.OnDisconnected(this, DisconnectReason.TransportError);
}
catch (NullReferenceException)
{
tryReceiveMore = false;
peer.OnDisconnected(this, DisconnectReason.TransportError);
}
if (byteCount > 0)
peer.OnDataReceived(byteCount, this);
}
}
/// <summary>Receives a message, if all of its data is ready to be received.</summary>
/// <param name="receivedByteCount">How many bytes were received.</param>
/// <returns>Whether or not all of the message's data was ready to be received.</returns>
private bool TryReceiveMessage(out int receivedByteCount)
{
if (socket.Available >= nextMessageSize)
{
// We have enough bytes to read the complete message
receivedByteCount = socket.Receive(peer.ReceiveBuffer, nextMessageSize, SocketFlags.None);
nextMessageSize = 0;
return true;
}
receivedByteCount = 0;
return false;
}
/// <summary>Closes the connection.</summary>
internal void Close()
{
socket.Close();
}
/// <inheritdoc/>
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
/// <inheritdoc/>
public override bool Equals(object obj) => Equals(obj as TcpConnection);
/// <inheritdoc/>
public bool Equals(TcpConnection 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 ==(TcpConnection left, TcpConnection 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 !=(TcpConnection left, TcpConnection right) => !(left == right);
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
}

View File

@@ -0,0 +1,56 @@
// 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.Sockets;
namespace Riptide.Transports.Tcp
{
/// <summary>Provides base send &#38; receive functionality for <see cref="TcpServer"/> and <see cref="TcpClient"/>.</summary>
public abstract class TcpPeer
{
/// <inheritdoc cref="IPeer.Disconnected"/>
public event EventHandler<DisconnectedEventArgs> Disconnected;
/// <summary>An array that incoming data is received into.</summary>
internal readonly byte[] ReceiveBuffer;
/// <summary>An array that outgoing data is sent out of.</summary>
internal readonly byte[] SendBuffer;
/// <summary>The default size used for the socket's send and receive buffers.</summary>
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
/// <summary>The size to use for the socket's send and receive buffers.</summary>
protected readonly int socketBufferSize;
/// <summary>The main socket, either used for listening for connections or for sending and receiving data.</summary>
protected Socket socket;
/// <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>Initializes the transport.</summary>
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
protected TcpPeer(int socketBufferSize = DefaultSocketBufferSize)
{
if (socketBufferSize < MinSocketBufferSize)
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
this.socketBufferSize = socketBufferSize;
ReceiveBuffer = new byte[Message.MaxSize];
SendBuffer = new byte[Message.MaxSize + sizeof(int)]; // Need room for the entire message plus the message length (since this is TCP)
}
/// <summary>Handles received data.</summary>
/// <param name="amount">The number of bytes that were received.</param>
/// <param name="fromConnection">The connection from which the data was received.</param>
protected internal abstract void OnDataReceived(int amount, TcpConnection fromConnection);
/// <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 internal virtual void OnDisconnected(Connection connection, DisconnectReason reason)
{
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
}
}
}

View File

@@ -0,0 +1,157 @@
// 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;
using System.Net.Sockets;
namespace Riptide.Transports.Tcp
{
/// <summary>A server which can accept connections from <see cref="TcpClient"/>s.</summary>
public class TcpServer : TcpPeer, IServer
{
/// <inheritdoc/>
public event EventHandler<ConnectedEventArgs> Connected;
/// <inheritdoc/>
public event EventHandler<DataReceivedEventArgs> DataReceived;
/// <inheritdoc/>
public ushort Port { get; private set; }
/// <summary>The maximum number of pending connections to allow at any given time.</summary>
public int MaxPendingConnections { get; private set; } = 5;
/// <summary>Whether or not the server is running.</summary>
private bool isRunning = false;
/// <summary>The currently open connections, accessible by their endpoints.</summary>
private Dictionary<IPEndPoint, TcpConnection> connections;
/// <summary>Connections that have been closed and need to be removed from <see cref="connections"/>.</summary>
private readonly List<IPEndPoint> closedConnections = new List<IPEndPoint>();
/// <summary>The IP address to bind the socket to.</summary>
private readonly IPAddress listenAddress;
/// <inheritdoc/>
public TcpServer(int socketBufferSize = DefaultSocketBufferSize) : this(IPAddress.IPv6Any, 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 TcpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(socketBufferSize)
{
this.listenAddress = listenAddress;
}
/// <inheritdoc/>
public void Start(ushort port)
{
Port = port;
connections = new Dictionary<IPEndPoint, TcpConnection>();
StartListening(port);
}
/// <summary>Starts listening for connections on the given port.</summary>
/// <param name="port">The port to listen on.</param>
private void StartListening(ushort port)
{
if (isRunning)
StopListening();
IPEndPoint localEndPoint = new IPEndPoint(listenAddress, port);
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
{
SendBufferSize = socketBufferSize,
ReceiveBufferSize = socketBufferSize,
NoDelay = true,
};
socket.Bind(localEndPoint);
socket.Listen(MaxPendingConnections);
isRunning = true;
}
/// <inheritdoc/>
public void Poll()
{
if (!isRunning)
return;
Accept();
foreach (TcpConnection connection in connections.Values)
connection.Receive();
foreach (IPEndPoint endPoint in closedConnections)
connections.Remove(endPoint);
closedConnections.Clear();
}
/// <summary>Accepts any pending connections.</summary>
private void Accept()
{
if (socket.Poll(0, SelectMode.SelectRead))
{
Socket acceptedSocket = socket.Accept();
IPEndPoint fromEndPoint = (IPEndPoint)acceptedSocket.RemoteEndPoint;
if (!connections.ContainsKey(fromEndPoint))
{
TcpConnection newConnection = new TcpConnection(acceptedSocket, fromEndPoint, this);
connections.Add(fromEndPoint, newConnection);
OnConnected(newConnection);
}
else
acceptedSocket.Close();
}
}
/// <summary>Stops listening for connections.</summary>
private void StopListening()
{
if (!isRunning)
return;
isRunning = false;
socket.Close();
}
/// <inheritdoc/>
public void Close(Connection connection)
{
if (connection is TcpConnection tcpConnection)
{
closedConnections.Add(tcpConnection.RemoteEndPoint);
tcpConnection.Close();
}
}
/// <inheritdoc/>
public void Shutdown()
{
StopListening();
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 internal override void OnDataReceived(int amount, TcpConnection fromConnection)
{
if ((MessageHeader)(ReceiveBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect)
{
if (fromConnection.DidReceiveConnect)
return;
fromConnection.DidReceiveConnect = true;
}
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
}
}
}