first commit
This commit is contained in:
61
Riptide/Transports/EventArgs.cs
Normal file
61
Riptide/Transports/EventArgs.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Contains event data for when a server's transport successfully establishes a connection to a client.</summary>
|
||||
public class ConnectedEventArgs
|
||||
{
|
||||
/// <summary>The newly established connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The newly established connection.</param>
|
||||
public ConnectedEventArgs(Connection connection)
|
||||
{
|
||||
Connection = connection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport receives data.</summary>
|
||||
public class DataReceivedEventArgs
|
||||
{
|
||||
/// <summary>An array containing the received data.</summary>
|
||||
public readonly byte[] DataBuffer;
|
||||
/// <summary>The number of bytes that were received.</summary>
|
||||
public readonly int Amount;
|
||||
/// <summary>The connection which the data was received from.</summary>
|
||||
public readonly Connection FromConnection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="dataBuffer">An array containing the received data.</param>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="fromConnection">The connection which the data was received from.</param>
|
||||
public DataReceivedEventArgs(byte[] dataBuffer, int amount, Connection fromConnection)
|
||||
{
|
||||
DataBuffer = dataBuffer;
|
||||
Amount = amount;
|
||||
FromConnection = fromConnection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport initiates or detects a disconnection.</summary>
|
||||
public class DisconnectedEventArgs
|
||||
{
|
||||
/// <summary>The closed connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
public DisconnectedEventArgs(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Connection = connection;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Riptide/Transports/IClient.cs
Normal file
28
Riptide/Transports/IClient.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's client must implement.</summary>
|
||||
public interface IClient : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler Connected;
|
||||
/// <summary>Invoked when a connection attempt fails at the transport level.</summary>
|
||||
event EventHandler ConnectionFailed;
|
||||
|
||||
/// <summary>Starts the transport and attempts to connect to the given host address.</summary>
|
||||
/// <param name="hostAddress">The host address to connect to.</param>
|
||||
/// <param name="connection">The pending connection. <see langword="null"/> if an issue occurred.</param>
|
||||
/// <param name="connectError">The error message associated with the issue that occurred, if any.</param>
|
||||
/// <returns><see langword="true"/> if a connection attempt will be made. <see langword="false"/> if an issue occurred (such as <paramref name="hostAddress"/> being in an invalid format) and a connection attempt will <i>not</i> be made.</returns>
|
||||
bool Connect(string hostAddress, out Connection connection, out string connectError);
|
||||
|
||||
/// <summary>Closes the connection to the server.</summary>
|
||||
void Disconnect();
|
||||
}
|
||||
}
|
||||
50
Riptide/Transports/IPeer.cs
Normal file
50
Riptide/Transports/IPeer.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>The header type of a <see cref="Message"/>.</summary>
|
||||
public enum MessageHeader : byte
|
||||
{
|
||||
/// <summary>An unreliable user message.</summary>
|
||||
Unreliable,
|
||||
/// <summary>An internal unreliable ack message.</summary>
|
||||
Ack,
|
||||
/// <summary>An internal unreliable connect message.</summary>
|
||||
Connect,
|
||||
/// <summary>An internal unreliable connection rejection message.</summary>
|
||||
Reject,
|
||||
/// <summary>An internal unreliable heartbeat message.</summary>
|
||||
Heartbeat,
|
||||
/// <summary>An internal unreliable disconnect message.</summary>
|
||||
Disconnect,
|
||||
|
||||
/// <summary>A notify message.</summary>
|
||||
Notify,
|
||||
|
||||
/// <summary>A reliable user message.</summary>
|
||||
Reliable,
|
||||
/// <summary>An internal reliable welcome message.</summary>
|
||||
Welcome,
|
||||
/// <summary>An internal reliable client connected message.</summary>
|
||||
ClientConnected,
|
||||
/// <summary>An internal reliable client disconnected message.</summary>
|
||||
ClientDisconnected,
|
||||
}
|
||||
|
||||
/// <summary>Defines methods, properties, and events which every transport's server <i>and</i> client must implement.</summary>
|
||||
public interface IPeer
|
||||
{
|
||||
/// <summary>Invoked when data is received by the transport.</summary>
|
||||
event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
/// <summary>Invoked when a disconnection is initiated or detected by the transport.</summary>
|
||||
event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>Initiates handling of any received messages.</summary>
|
||||
void Poll();
|
||||
}
|
||||
}
|
||||
30
Riptide/Transports/IServer.cs
Normal file
30
Riptide/Transports/IServer.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's server must implement.</summary>
|
||||
public interface IServer : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler<ConnectedEventArgs> Connected;
|
||||
|
||||
/// <inheritdoc cref="Server.Port"/>
|
||||
ushort Port { get; }
|
||||
|
||||
/// <summary>Starts the transport and begins listening for incoming connections.</summary>
|
||||
/// <param name="port">The local port on which to listen for connections.</param>
|
||||
void Start(ushort port);
|
||||
|
||||
/// <summary>Closes an active connection.</summary>
|
||||
/// <param name="connection">The connection to close.</param>
|
||||
void Close(Connection connection);
|
||||
|
||||
/// <summary>Closes all existing connections and stops listening for new connections.</summary>
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
120
Riptide/Transports/Tcp/TcpClient.cs
Normal file
120
Riptide/Transports/Tcp/TcpClient.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Riptide/Transports/Tcp/TcpConnection.cs
Normal file
195
Riptide/Transports/Tcp/TcpConnection.cs
Normal 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
|
||||
}
|
||||
}
|
||||
56
Riptide/Transports/Tcp/TcpPeer.cs
Normal file
56
Riptide/Transports/Tcp/TcpPeer.cs
Normal 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 & 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Riptide/Transports/Tcp/TcpServer.cs
Normal file
157
Riptide/Transports/Tcp/TcpServer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Riptide/Transports/Udp/UdpClient.cs
Normal file
111
Riptide/Transports/Udp/UdpClient.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Riptide/Transports/Udp/UdpConnection.cs
Normal file
80
Riptide/Transports/Udp/UdpConnection.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
185
Riptide/Transports/Udp/UdpPeer.cs
Normal file
185
Riptide/Transports/Udp/UdpPeer.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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 & 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Riptide/Transports/Udp/UdpServer.cs
Normal file
93
Riptide/Transports/Udp/UdpServer.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user