-
-
Notifications
You must be signed in to change notification settings - Fork 141
/
Client.cs
418 lines (373 loc) · 20.3 KB
/
Client.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// 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
}
}