mirror of
https://github.com/sbrl/Nibriboard.git
synced 2018-01-10 21:33:49 +00:00
Put some infrastructure in palce to better handle disconnects and shutdowns.
This commit is contained in:
parent
66b16acd3d
commit
686dd2f56d
9 changed files with 163 additions and 3 deletions
16
Nibriboard/Client/Messages/HeartbeatMessage.cs
Normal file
16
Nibriboard/Client/Messages/HeartbeatMessage.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nibriboard.Client.Messages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a heartbeat message. The server sends this periodically to the client,
|
||||||
|
/// to which the client should respond with an identical message.
|
||||||
|
/// </summary>
|
||||||
|
public class HeartbeatMessage : Message
|
||||||
|
{
|
||||||
|
public HeartbeatMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
Nibriboard/Client/Messages/IdleDisconnectMessage.cs
Normal file
15
Nibriboard/Client/Messages/IdleDisconnectMessage.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nibriboard.Client.Messages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sent by the server to an idle client who's not responding to heartbeats just before the client disconnects them.
|
||||||
|
/// </summary>
|
||||||
|
public class IdleDisconnectMessage : Message
|
||||||
|
{
|
||||||
|
public IdleDisconnectMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,13 @@ namespace Nibriboard.Client.Messages
|
||||||
{
|
{
|
||||||
public class Message
|
public class Message
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the event that this message is about.
|
||||||
|
/// </summary>
|
||||||
public readonly string Event = string.Empty;
|
public readonly string Event = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// The date and time that this message was sent.
|
||||||
|
/// </summary>
|
||||||
public readonly DateTime TimeSent = DateTime.Now;
|
public readonly DateTime TimeSent = DateTime.Now;
|
||||||
|
|
||||||
public Message()
|
public Message()
|
||||||
|
|
15
Nibriboard/Client/Messages/ShutdownMessage.cs
Normal file
15
Nibriboard/Client/Messages/ShutdownMessage.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nibriboard.Client.Messages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A message that's sent by the server to a client to tell them that the server is shutting down.
|
||||||
|
/// </summary>
|
||||||
|
public class ShutdownMessage : Message
|
||||||
|
{
|
||||||
|
public ShutdownMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -52,10 +52,26 @@ namespace Nibriboard.Client
|
||||||
public bool Connected = true;
|
public bool Connected = true;
|
||||||
public event NibriDisconnectedEvent Disconnected;
|
public event NibriDisconnectedEvent Disconnected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date and time at which the last message was received from this client.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastMessageTime = DateTime.Now;
|
||||||
|
/// <summary>
|
||||||
|
/// The number of milliseconds since we last received a message from this client.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The milliseconds since last message.</value>
|
||||||
|
public int MillisecondsSinceLastMessage {
|
||||||
|
get {
|
||||||
|
return (int)((DateTime.Now - LastMessageTime).TotalMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this client has completed the handshake yet or not.
|
/// Whether this client has completed the handshake yet or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HandshakeCompleted = false;
|
public bool HandshakeCompleted = false;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name this client has assignedto themselves.
|
/// The name this client has assignedto themselves.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -102,18 +118,22 @@ namespace Nibriboard.Client
|
||||||
client.ConnectionClosed += (WebSocket socket) => {
|
client.ConnectionClosed += (WebSocket socket) => {
|
||||||
Connected = false;
|
Connected = false;
|
||||||
Disconnected(this);
|
Disconnected(this);
|
||||||
|
Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task handleMessage(string frame)
|
private async Task handleMessage(string frame)
|
||||||
{
|
{
|
||||||
|
// Updatet he last time we received a message from the client
|
||||||
|
LastMessageTime = DateTime.Now;
|
||||||
|
|
||||||
|
// Extract the event name from the message that the client sent.
|
||||||
string eventName = JsonUtilities.DeserializeProperty<string>(frame, "Event");
|
string eventName = JsonUtilities.DeserializeProperty<string>(frame, "Event");
|
||||||
|
|
||||||
if(eventName == null) {
|
if(eventName == null) {
|
||||||
Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id);
|
Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!messageEventTypes.ContainsKey(eventName)) {
|
if (!messageEventTypes.ContainsKey(eventName)) {
|
||||||
Log.WriteLine("[NibriClient#{0}] Received message with invalid event {1}.", Id, eventName);
|
Log.WriteLine("[NibriClient#{0}] Received message with invalid event {1}.", Id, eventName);
|
||||||
return;
|
return;
|
||||||
|
@ -155,6 +175,28 @@ namespace Nibriboard.Client
|
||||||
client.Send(message);
|
client.Send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a heartbeat message to this client.
|
||||||
|
/// </summary>
|
||||||
|
public void SendHeartbeat()
|
||||||
|
{
|
||||||
|
Send(new HeartbeatMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the connection to the client gracefully.
|
||||||
|
/// </summary>
|
||||||
|
public void CloseConnection(Message lastMessage)
|
||||||
|
{
|
||||||
|
if (!Connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Tell the client that we're shutting down
|
||||||
|
Send(lastMessage);
|
||||||
|
|
||||||
|
client.Close();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a new ClientState object representing this client's state at the current time.
|
/// Generates a new ClientState object representing this client's state at the current time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Nibriboard.Client.Messages;
|
using Nibriboard.Client.Messages;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Nibriboard.Client
|
namespace Nibriboard.Client
|
||||||
{
|
{
|
||||||
|
@ -15,6 +16,16 @@ namespace Nibriboard.Client
|
||||||
private ClientSettings clientSettings;
|
private ClientSettings clientSettings;
|
||||||
public List<NibriClient> Clients = new List<NibriClient>();
|
public List<NibriClient> Clients = new List<NibriClient>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cancellation token that's used by the main server to tell us when we should shut down.
|
||||||
|
/// </summary>
|
||||||
|
protected CancellationToken canceller;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The interval at which heatbeats should be sent to the client.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int HeatbeatInterval = 5000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of clients currently connected to this Nibriboard.
|
/// The number of clients currently connected to this Nibriboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -24,9 +35,10 @@ namespace Nibriboard.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public NibriClientManager(ClientSettings inClientSettings)
|
public NibriClientManager(ClientSettings inClientSettings, CancellationToken inCancellationToken)
|
||||||
{
|
{
|
||||||
clientSettings = inClientSettings;
|
clientSettings = inClientSettings;
|
||||||
|
canceller = inCancellationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -71,6 +83,41 @@ namespace Nibriboard.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ClientMaintenanceMonkey()
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
// Exit if we've been asked to shut down
|
||||||
|
if (canceller.IsCancellationRequested) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect unresponsive clients.
|
||||||
|
foreach (NibriClient client in Clients) {
|
||||||
|
// If we haven't heard from this client in a little while, send a heartbeat message
|
||||||
|
if(client.MillisecondsSinceLastMessage > HeatbeatInterval)
|
||||||
|
client.SendHeartbeat();
|
||||||
|
|
||||||
|
// If the client hasn't sent us a message in a while (even though we sent
|
||||||
|
// them a heartbeat to check on them on the last loop), disconnect them
|
||||||
|
if (client.MillisecondsSinceLastMessage > HeatbeatInterval * 2)
|
||||||
|
client.CloseConnection(new IdleDisconnectMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(HeatbeatInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up this NibriClient manager ready for shutdown.
|
||||||
|
/// </summary>
|
||||||
|
private void close()
|
||||||
|
{
|
||||||
|
// Close the connection to all the remaining nibri clients, telling them that the server is about to shut down
|
||||||
|
foreach (NibriClient client in Clients)
|
||||||
|
client.CloseConnection(new ShutdownMessage());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clean up after a client disconnects from the server.
|
/// Clean up after a client disconnects from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -20,6 +20,9 @@ class RippleLink extends EventEmitter
|
||||||
this.websocket.addEventListener("message", this.handleMessage.bind(this));
|
this.websocket.addEventListener("message", this.handleMessage.bind(this));
|
||||||
this.websocket.addEventListener("close", this.handleDisconnection.bind(this));
|
this.websocket.addEventListener("close", this.handleDisconnection.bind(this));
|
||||||
|
|
||||||
|
// Respond to heartbeats from the server
|
||||||
|
this.on("Heartbeat", this.handleHeartbeat.bind(this));
|
||||||
|
|
||||||
// Close the socket correctly
|
// Close the socket correctly
|
||||||
window.addEventListener("beforeunload", (function(event) {
|
window.addEventListener("beforeunload", (function(event) {
|
||||||
this.websocket.close();
|
this.websocket.close();
|
||||||
|
@ -46,6 +49,16 @@ class RippleLink extends EventEmitter
|
||||||
this.emit(message.Event, message);
|
this.emit(message.Event, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replies to heartbeat messages from the server.
|
||||||
|
*/
|
||||||
|
handleHeartbeat(message) {
|
||||||
|
// Reply with a heartbeat
|
||||||
|
this.send({
|
||||||
|
"Event": "Heartbeat"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message object to the server.
|
* Sends a message object to the server.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -82,6 +82,9 @@
|
||||||
<Compile Include="Utilities\ColourHSL.cs" />
|
<Compile Include="Utilities\ColourHSL.cs" />
|
||||||
<Compile Include="Utilities\ToStringJsonConverter.cs" />
|
<Compile Include="Utilities\ToStringJsonConverter.cs" />
|
||||||
<Compile Include="Client\Messages\HandshakeResponseMessage.cs" />
|
<Compile Include="Client\Messages\HandshakeResponseMessage.cs" />
|
||||||
|
<Compile Include="Client\Messages\ShutdownMessage.cs" />
|
||||||
|
<Compile Include="Client\Messages\IdleDisconnectMessage.cs" />
|
||||||
|
<Compile Include="Client\Messages\HeartbeatMessage.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="ClientFiles\index.html" />
|
<EmbeddedResource Include="ClientFiles\index.html" />
|
||||||
|
|
|
@ -6,6 +6,7 @@ using IotWeb.Common.Http;
|
||||||
|
|
||||||
using Nibriboard.RippleSpace;
|
using Nibriboard.RippleSpace;
|
||||||
using Nibriboard.Client;
|
using Nibriboard.Client;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Nibriboard
|
namespace Nibriboard
|
||||||
{
|
{
|
||||||
|
@ -20,6 +21,8 @@ namespace Nibriboard
|
||||||
private ClientSettings clientSettings;
|
private ClientSettings clientSettings;
|
||||||
private RippleSpaceManager planeManager = new RippleSpaceManager();
|
private RippleSpaceManager planeManager = new RippleSpaceManager();
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource clientManagerCanceller = new CancellationTokenSource();
|
||||||
|
|
||||||
public readonly int Port = 31586;
|
public readonly int Port = 31586;
|
||||||
|
|
||||||
public NibriboardServer(int inPort = 31586)
|
public NibriboardServer(int inPort = 31586)
|
||||||
|
@ -51,7 +54,7 @@ namespace Nibriboard
|
||||||
// Websocket setup
|
// Websocket setup
|
||||||
httpServer.AddWebSocketRequestHandler(
|
httpServer.AddWebSocketRequestHandler(
|
||||||
clientSettings.WebsocketPath,
|
clientSettings.WebsocketPath,
|
||||||
new NibriClientManager(clientSettings)
|
new NibriClientManager(clientSettings, clientManagerCanceller.Token)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue