419 lines
20 KiB
C#
419 lines
20 KiB
C#
// 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.Transports;
|
|
using Riptide.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
|
|
namespace Riptide
|
|
{
|
|
/// <summary>A client that can connect to a <see cref="Server"/>.</summary>
|
|
public class Client : Peer
|
|
{
|
|
/// <summary>Invoked when a connection to the server is established.</summary>
|
|
public event EventHandler Connected;
|
|
/// <summary>Invoked when a connection to the server fails to be established.</summary>
|
|
public event EventHandler<ConnectionFailedEventArgs> ConnectionFailed;
|
|
/// <summary>Invoked when a message is received.</summary>
|
|
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
|
/// <summary>Invoked when disconnected from the server.</summary>
|
|
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
|
/// <summary>Invoked when another <i>non-local</i> client connects.</summary>
|
|
public event EventHandler<ClientConnectedEventArgs> ClientConnected;
|
|
/// <summary>Invoked when another <i>non-local</i> client disconnects.</summary>
|
|
public event EventHandler<ClientDisconnectedEventArgs> ClientDisconnected;
|
|
|
|
/// <summary>The client's numeric ID.</summary>
|
|
public ushort Id => connection.Id;
|
|
/// <inheritdoc cref="Connection.RTT"/>
|
|
public short RTT => connection.RTT;
|
|
/// <inheritdoc cref="Connection.SmoothRTT"/>
|
|
/// <remarks>This value is slower to accurately represent lasting changes in latency than <see cref="RTT"/>, but it is less susceptible to changing drastically due to significant—but temporary—jumps in latency.</remarks>
|
|
public short SmoothRTT => connection.SmoothRTT;
|
|
/// <summary>Sets the client's <see cref="Connection.TimeoutTime"/>.</summary>
|
|
public override int TimeoutTime
|
|
{
|
|
set
|
|
{
|
|
defaultTimeout = value;
|
|
connection.TimeoutTime = defaultTimeout;
|
|
}
|
|
}
|
|
/// <summary>Whether or not the client is currently <i>not</i> trying to connect, pending, nor actively connected.</summary>
|
|
public bool IsNotConnected => connection is null || connection.IsNotConnected;
|
|
/// <summary>Whether or not the client is currently in the process of connecting.</summary>
|
|
public bool IsConnecting => !(connection is null) && connection.IsConnecting;
|
|
/// <summary>Whether or not the client's connection is currently pending (waiting to be accepted/rejected by the server).</summary>
|
|
public bool IsPending => !(connection is null) && connection.IsPending;
|
|
/// <summary>Whether or not the client is currently connected.</summary>
|
|
public bool IsConnected => !(connection is null) && connection.IsConnected;
|
|
/// <summary>The client's connection to a server.</summary>
|
|
// Not an auto property because properties can't be passed as ref/out parameters. Could
|
|
// use a local variable in the Connect method, but that's arguably not any cleaner. This
|
|
// property will also probably only be used rarely from outside the class/library.
|
|
public Connection Connection => connection;
|
|
/// <summary>Encapsulates a method that handles a message from a server.</summary>
|
|
/// <param name="message">The message that was received.</param>
|
|
public delegate void MessageHandler(Message message);
|
|
|
|
/// <inheritdoc cref="Connection"/>
|
|
private Connection connection;
|
|
/// <summary>How many connection attempts have been made so far.</summary>
|
|
private int connectionAttempts;
|
|
/// <summary>How many connection attempts to make before giving up.</summary>
|
|
private int maxConnectionAttempts;
|
|
/// <inheritdoc cref="Server.messageHandlers"/>
|
|
private Dictionary<ushort, MessageHandler> messageHandlers;
|
|
/// <summary>The underlying transport's client that is used for sending and receiving data.</summary>
|
|
private IClient transport;
|
|
/// <summary>The message sent when connecting. May include custom data.</summary>
|
|
private Message connectMessage;
|
|
|
|
/// <summary>Handles initial setup.</summary>
|
|
/// <param name="transport">The transport to use for sending and receiving data.</param>
|
|
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
|
public Client(IClient transport, string logName = "CLIENT") : base(logName)
|
|
{
|
|
this.transport = transport;
|
|
}
|
|
/// <summary>Handles initial setup using the built-in UDP transport.</summary>
|
|
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
|
public Client(string logName = "CLIENT") : this(new Transports.Udp.UdpClient(), logName) { }
|
|
|
|
/// <summary>Disconnects the client if it's connected and swaps out the transport it's using.</summary>
|
|
/// <param name="newTransport">The new transport to use for sending and receiving data.</param>
|
|
/// <remarks>This method does not automatically reconnect to the server. To continue communicating with the server, <see cref="Connect(string, int, byte, Message, bool)"/> must be called again.</remarks>
|
|
public void ChangeTransport(IClient newTransport)
|
|
{
|
|
Disconnect();
|
|
transport = newTransport;
|
|
}
|
|
|
|
/// <summary>Attempts to connect to a server at the given host address.</summary>
|
|
/// <param name="hostAddress">The host address to connect to.</param>
|
|
/// <param name="maxConnectionAttempts">How many connection attempts to make before giving up.</param>
|
|
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to use when building <see cref="messageHandlers"/>.</param>
|
|
/// <param name="message">Data that should be sent to the server with the connection attempt. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
|
/// <param name="useMessageHandlers">Whether or not the client should use the built-in message handler system.</param>
|
|
/// <remarks>
|
|
/// <para>Riptide's default transport expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>. If you are using a different transport, check the relevant documentation for what information it requires in the host address.</para>
|
|
/// <para>Setting <paramref name="useMessageHandlers"/> to <see langword="false"/> will disable the automatic detection and execution of methods with the <see cref="MessageHandlerAttribute"/>, which is beneficial if you prefer to handle messages via the <see cref="MessageReceived"/> event.</para>
|
|
/// </remarks>
|
|
/// <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>
|
|
public bool Connect(string hostAddress, int maxConnectionAttempts = 5, byte messageHandlerGroupId = 0, Message message = null, bool useMessageHandlers = true)
|
|
{
|
|
Disconnect();
|
|
|
|
SubToTransportEvents();
|
|
|
|
if (!transport.Connect(hostAddress, out connection, out string connectError))
|
|
{
|
|
RiptideLogger.Log(LogType.Error, LogName, connectError);
|
|
UnsubFromTransportEvents();
|
|
return false;
|
|
}
|
|
|
|
this.maxConnectionAttempts = maxConnectionAttempts;
|
|
connectionAttempts = 0;
|
|
connection.Initialize(this, defaultTimeout);
|
|
IncreaseActiveCount();
|
|
this.useMessageHandlers = useMessageHandlers;
|
|
if (useMessageHandlers)
|
|
CreateMessageHandlersDictionary(messageHandlerGroupId);
|
|
|
|
connectMessage = Message.Create(MessageHeader.Connect);
|
|
if (message != null)
|
|
{
|
|
if (message.ReadBits != 0)
|
|
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting connection attempt data!");
|
|
|
|
connectMessage.AddMessage(message);
|
|
message.Release();
|
|
}
|
|
|
|
StartTime();
|
|
Heartbeat();
|
|
RiptideLogger.Log(LogType.Info, LogName, $"Connecting to {connection}...");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Subscribes appropriate methods to the transport's events.</summary>
|
|
private void SubToTransportEvents()
|
|
{
|
|
transport.Connected += TransportConnected;
|
|
transport.ConnectionFailed += TransportConnectionFailed;
|
|
transport.DataReceived += HandleData;
|
|
transport.Disconnected += TransportDisconnected;
|
|
}
|
|
|
|
/// <summary>Unsubscribes methods from all of the transport's events.</summary>
|
|
private void UnsubFromTransportEvents()
|
|
{
|
|
transport.Connected -= TransportConnected;
|
|
transport.ConnectionFailed -= TransportConnectionFailed;
|
|
transport.DataReceived -= HandleData;
|
|
transport.Disconnected -= TransportDisconnected;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void CreateMessageHandlersDictionary(byte messageHandlerGroupId)
|
|
{
|
|
MethodInfo[] methods = FindMessageHandlers();
|
|
|
|
messageHandlers = new Dictionary<ushort, MessageHandler>(methods.Length);
|
|
foreach (MethodInfo method in methods)
|
|
{
|
|
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>();
|
|
if (attribute.GroupId != messageHandlerGroupId)
|
|
continue;
|
|
|
|
if (!method.IsStatic)
|
|
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
|
|
|
Delegate clientMessageHandler = Delegate.CreateDelegate(typeof(MessageHandler), method, false);
|
|
if (clientMessageHandler != null)
|
|
{
|
|
// It's a message handler for Client instances
|
|
if (messageHandlers.ContainsKey(attribute.MessageId))
|
|
{
|
|
MethodInfo otherMethodWithId = messageHandlers[attribute.MessageId].GetMethodInfo();
|
|
throw new DuplicateHandlerException(attribute.MessageId, method, otherMethodWithId);
|
|
}
|
|
else
|
|
messageHandlers.Add(attribute.MessageId, (MessageHandler)clientMessageHandler);
|
|
}
|
|
else
|
|
{
|
|
// It's not a message handler for Client instances, but it might be one for Server instances
|
|
if (Delegate.CreateDelegate(typeof(Server.MessageHandler), method, false) == null)
|
|
throw new InvalidHandlerSignatureException(method.DeclaringType, method.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
internal override void Heartbeat()
|
|
{
|
|
if (IsConnecting)
|
|
{
|
|
// If still trying to connect, send connect messages instead of heartbeats
|
|
if (connectionAttempts < maxConnectionAttempts)
|
|
{
|
|
Send(connectMessage, false);
|
|
connectionAttempts++;
|
|
}
|
|
else
|
|
LocalDisconnect(DisconnectReason.NeverConnected);
|
|
}
|
|
else if (IsPending)
|
|
{
|
|
// If waiting for the server to accept/reject the connection attempt
|
|
if (connection.HasConnectAttemptTimedOut)
|
|
{
|
|
LocalDisconnect(DisconnectReason.TimedOut);
|
|
return;
|
|
}
|
|
}
|
|
else if (IsConnected)
|
|
{
|
|
// If connected and not timed out, send heartbeats
|
|
if (connection.HasTimedOut)
|
|
{
|
|
LocalDisconnect(DisconnectReason.TimedOut);
|
|
return;
|
|
}
|
|
|
|
connection.SendHeartbeat();
|
|
}
|
|
|
|
ExecuteLater(HeartbeatInterval, new HeartbeatEvent(this));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void Update()
|
|
{
|
|
base.Update();
|
|
transport.Poll();
|
|
HandleMessages();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void Handle(Message message, MessageHeader header, Connection connection)
|
|
{
|
|
switch (header)
|
|
{
|
|
// User messages
|
|
case MessageHeader.Unreliable:
|
|
case MessageHeader.Reliable:
|
|
OnMessageReceived(message);
|
|
break;
|
|
|
|
// Internal messages
|
|
case MessageHeader.Ack:
|
|
connection.HandleAck(message);
|
|
break;
|
|
case MessageHeader.Connect:
|
|
connection.SetPending();
|
|
break;
|
|
case MessageHeader.Reject:
|
|
if (!IsConnected) // Don't disconnect if we are connected
|
|
LocalDisconnect(DisconnectReason.ConnectionRejected, message, (RejectReason)message.GetByte());
|
|
break;
|
|
case MessageHeader.Heartbeat:
|
|
connection.HandleHeartbeatResponse(message);
|
|
break;
|
|
case MessageHeader.Disconnect:
|
|
LocalDisconnect((DisconnectReason)message.GetByte(), message);
|
|
break;
|
|
case MessageHeader.Welcome:
|
|
if (IsConnecting || IsPending)
|
|
{
|
|
connection.HandleWelcome(message);
|
|
OnConnected();
|
|
}
|
|
break;
|
|
case MessageHeader.ClientConnected:
|
|
OnClientConnected(message.GetUShort());
|
|
break;
|
|
case MessageHeader.ClientDisconnected:
|
|
OnClientDisconnected(message.GetUShort());
|
|
break;
|
|
default:
|
|
RiptideLogger.Log(LogType.Warning, LogName, $"Unexpected message header '{header}'! Discarding {message.BytesInUse} bytes.");
|
|
break;
|
|
}
|
|
|
|
message.Release();
|
|
}
|
|
|
|
/// <summary>Sends a message to the server.</summary>
|
|
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
|
public ushort Send(Message message, bool shouldRelease = true) => connection.Send(message, shouldRelease);
|
|
|
|
/// <summary>Disconnects from the server.</summary>
|
|
public void Disconnect()
|
|
{
|
|
if (connection == null || IsNotConnected)
|
|
return;
|
|
|
|
Send(Message.Create(MessageHeader.Disconnect));
|
|
LocalDisconnect(DisconnectReason.Disconnected);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
internal override void Disconnect(Connection connection, DisconnectReason reason)
|
|
{
|
|
if (connection.IsConnected && connection.CanQualityDisconnect)
|
|
LocalDisconnect(reason);
|
|
}
|
|
|
|
/// <summary>Cleans up the local side of the connection.</summary>
|
|
/// <param name="reason">The reason why the client has disconnected.</param>
|
|
/// <param name="message">The disconnection or rejection message, potentially containing extra data to be handled externally.</param>
|
|
/// <param name="rejectReason">The reason why the connection was rejected (<i>if</i> it was rejected).</param>
|
|
private void LocalDisconnect(DisconnectReason reason, Message message = null, RejectReason rejectReason = RejectReason.NoConnection)
|
|
{
|
|
if (IsNotConnected)
|
|
return;
|
|
|
|
UnsubFromTransportEvents();
|
|
DecreaseActiveCount();
|
|
|
|
StopTime();
|
|
transport.Disconnect();
|
|
|
|
connection.LocalDisconnect();
|
|
|
|
if (reason == DisconnectReason.NeverConnected)
|
|
OnConnectionFailed(RejectReason.NoConnection);
|
|
else if (reason == DisconnectReason.ConnectionRejected)
|
|
OnConnectionFailed(rejectReason, message);
|
|
else
|
|
OnDisconnected(reason, message);
|
|
}
|
|
|
|
/// <summary>What to do when the transport establishes a connection.</summary>
|
|
private void TransportConnected(object sender, EventArgs e) { }
|
|
|
|
/// <summary>What to do when the transport fails to connect.</summary>
|
|
private void TransportConnectionFailed(object sender, EventArgs e)
|
|
{
|
|
LocalDisconnect(DisconnectReason.NeverConnected);
|
|
}
|
|
|
|
/// <summary>What to do when the transport disconnects.</summary>
|
|
private void TransportDisconnected(object sender, Transports.DisconnectedEventArgs e)
|
|
{
|
|
if (connection == e.Connection)
|
|
LocalDisconnect(e.Reason);
|
|
}
|
|
|
|
#region Events
|
|
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
|
protected virtual void OnConnected()
|
|
{
|
|
connectMessage.Release();
|
|
connectMessage = null;
|
|
RiptideLogger.Log(LogType.Info, LogName, "Connected successfully!");
|
|
Connected?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
|
/// <param name="reason">The reason for the connection failure.</param>
|
|
/// <param name="message">Additional data related to the failed connection attempt.</param>
|
|
protected virtual void OnConnectionFailed(RejectReason reason, Message message = null)
|
|
{
|
|
connectMessage.Release();
|
|
connectMessage = null;
|
|
RiptideLogger.Log(LogType.Info, LogName, $"Connection to server failed: {Helper.GetReasonString(reason)}.");
|
|
ConnectionFailed?.Invoke(this, new ConnectionFailedEventArgs(reason, message));
|
|
}
|
|
|
|
/// <summary>Invokes the <see cref="MessageReceived"/> event and initiates handling of the received message.</summary>
|
|
/// <param name="message">The received message.</param>
|
|
protected virtual void OnMessageReceived(Message message)
|
|
{
|
|
ushort messageId = (ushort)message.GetVarULong();
|
|
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(connection, messageId, message));
|
|
|
|
if (useMessageHandlers)
|
|
{
|
|
if (messageHandlers.TryGetValue(messageId, out MessageHandler messageHandler))
|
|
messageHandler(message);
|
|
else
|
|
RiptideLogger.Log(LogType.Warning, LogName, $"No message handler method found for message ID {messageId}!");
|
|
}
|
|
}
|
|
|
|
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
|
/// <param name="reason">The reason for the disconnection.</param>
|
|
/// <param name="message">Additional data related to the disconnection.</param>
|
|
protected virtual void OnDisconnected(DisconnectReason reason, Message message)
|
|
{
|
|
RiptideLogger.Log(LogType.Info, LogName, $"Disconnected from server: {Helper.GetReasonString(reason)}.");
|
|
Disconnected?.Invoke(this, new DisconnectedEventArgs(reason, message));
|
|
}
|
|
|
|
/// <summary>Invokes the <see cref="ClientConnected"/> event.</summary>
|
|
/// <param name="clientId">The numeric ID of the client that connected.</param>
|
|
protected virtual void OnClientConnected(ushort clientId)
|
|
{
|
|
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} connected.");
|
|
ClientConnected?.Invoke(this, new ClientConnectedEventArgs(clientId));
|
|
}
|
|
|
|
/// <summary>Invokes the <see cref="ClientDisconnected"/> event.</summary>
|
|
/// <param name="clientId">The numeric ID of the client that disconnected.</param>
|
|
protected virtual void OnClientDisconnected(ushort clientId)
|
|
{
|
|
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} disconnected.");
|
|
ClientDisconnected?.Invoke(this, new ClientDisconnectedEventArgs(clientId));
|
|
}
|
|
#endregion
|
|
}
|
|
}
|