1
0
Fork 0
mirror of https://github.com/sbrl/Nibriboard.git synced 2018-01-10 21:33:49 +00:00

It sure does feel strangely both good and scary to be completely refactoring so many core classes.

This commit is contained in:
Starbeamrainbowlabs 2017-09-10 21:34:01 +01:00
parent afb6eb8dca
commit be258f63dc
8 changed files with 246 additions and 370 deletions

View file

@ -1,33 +0,0 @@
using System;
using System.IO;
using SBRL.GlidingSquirrel.Http;
using SBRL.GlidingSquirrel.Websocket;
using Newtonsoft.Json;
namespace Nibriboard.Client
{
public class HttpClientSettingsHandler : WebsocketServer
{
private ClientSettings settings;
public HttpClientSettingsHandler(ClientSettings inSettings)
{
settings = inSettings;
}
public void HandleRequest(string uri, HttpRequest request, HttpResponse response, HttpContext context) {
StreamWriter responseData = new StreamWriter(response.Content) { AutoFlush = true };
string settingsJson = JsonConvert.SerializeObject(settings);
response.ContentLength = settingsJson.Length;
response.Headers.Add("content-type", "application/json");
responseData.Write(settingsJson);
Log.WriteLine("[Http/ClientSettings] Sent settings");
}
}
}

View file

@ -1,89 +0,0 @@
using System;
using System.IO;
using MimeSharp;
using SBRL.GlidingSquirrel.Websocket;
using SBRL.Utilities;
using System.Collections.Generic;
using System.Reflection;
namespace Nibriboard.Client
{
public class HttpEmbeddedFileHandler : WebsocketsServer
{
private string filePrefix;
private Mime mimeTypeFinder = new Mime();
private Dictionary<string, string> mimeTypeOverrides = new Dictionary<string, string>() {
["application/xhtml+xml"] = "text/html",
["application/tei+xml"] = "image/x-icon"
};
private List<string> embeddedFiles = new List<string>(EmbeddedFiles.ResourceList);
public HttpEmbeddedFileHandler(string inFilePrefix)
{
filePrefix = inFilePrefix;
}
public void HandleRequest(string uri, HttpRequest request, HttpResponse response, HttpContext context) {
StreamWriter responseData = new StreamWriter(response.Content) { AutoFlush = true };
if (request.Method != HttpMethod.Get) {
response.ResponseCode = HttpResponseCode.MethodNotAllowed;
response.ContentType = "text/plain";
responseData.WriteLine("Error: That method isn't supported yet.");
logRequest(request, response);
return;
}
string expandedFilePath = getEmbeddedFileReference(request.URI);
if (!embeddedFiles.Contains(expandedFilePath)) {
expandedFilePath += "index.html";
}
if (!embeddedFiles.Contains(expandedFilePath)) {
response.ResponseCode = HttpResponseCode.NotFound;
response.ContentType = "text/plain";
responseData.WriteLine("Can't find {0}.", expandedFilePath);
logRequest(request, response);
return;
}
response.ContentType = getMimeType(expandedFilePath);
response.Headers.Add("content-type", response.ContentType);
byte[] embeddedFile = EmbeddedFiles.ReadAllBytes(expandedFilePath);
response.ContentLength = embeddedFile.Length;
try
{
response.Content.Write(embeddedFile, 0, embeddedFile.Length);
}
catch(Exception error)
{
Log.WriteLine($"[Nibriboard/EmbeddedFileHandler] Error: {error.Message} Details:");
Log.WriteLine(error.ToString());
}
logRequest(request, response);
}
protected string getEmbeddedFileReference(string uri) {
return filePrefix + "." + uri.TrimStart("/".ToCharArray()).Replace('/', '.');
}
protected string getMimeType(string uri) {
string mimeType = mimeTypeFinder.Lookup(uri);
foreach (KeyValuePair<string, string> mimeMapping in mimeTypeOverrides) {
if (mimeType == mimeMapping.Key)
mimeType = mimeMapping.Value;
}
return mimeType;
}
private void logRequest(HttpRequest request, HttpResponse response) {
Log.WriteLine("[Http/FileHandler] {0} {1} {2} {3}", response.ResponseCode.ResponseCode(), response.ContentType, request.Method.ToString().ToUpper(), request.URI);
}
}
}

View file

@ -5,12 +5,13 @@ using System.Text;
using System.Linq;
using System.Reflection;
using IotWeb.Common.Http;
using Newtonsoft.Json;
using SBRL.Utilities;
using Nibriboard.Client.Messages;
using Nibriboard.RippleSpace;
using SBRL.GlidingSquirrel.Websocket;
namespace Nibriboard.Client
{
/// <summary>
@ -38,7 +39,7 @@ namespace Nibriboard.Client
/// <summary>
/// The nibri client manager
/// </summary>
private readonly NibriClientManager manager;
private readonly NibriboardApp manager;
/// <summary>
/// The plane that this client is currently on.
/// </summary>
@ -48,7 +49,7 @@ namespace Nibriboard.Client
/// The underlying websocket connection to the client.
/// Please try not to call the send method on here - use the NibriClient Send() method instead.
/// </summary>
private readonly WebSocket client;
private readonly WebsocketClient connection;
private static readonly Dictionary<string, Type> messageEventTypes = new Dictionary<string, Type>() {
["HandshakeRequest"] = typeof(HandshakeRequestMessage),
@ -64,7 +65,11 @@ namespace Nibriboard.Client
/// <summary>
/// Whether this nibri client is still connected.
/// </summary>
public bool Connected = true;
public bool Connected {
get {
return connection.IsClosing;
}
}
/// <summary>
/// Fires when this nibri client disconnects.
/// </summary>
@ -117,41 +122,26 @@ namespace Nibriboard.Client
#region Core Setup & Message Routing Logic
public NibriClient(NibriClientManager inManager, WebSocket inClient)
public NibriClient(NibriboardApp inManager, WebsocketClient inClient)
{
Log.WriteLine("[Nibriboard/WebSocket] New NibriClient connected with id #{0}.", Id);
manager = inManager;
client = inClient;
connection = inClient;
client.DataReceived += async (WebSocket clientSocket, string frame) => {
try
{
await handleMessage(frame);
}
catch (Exception error)
{
await Console.Error.WriteLineAsync(error.ToString());
throw;
}
// Attach a few events
connection.OnDisconnection += handleDisconnection;
connection.OnTextMessage += handleMessage;
//Task.Run(async () => await onMessage(frame)).Wait();
};
// Store whether this NibriClient is still connected or not
client.ConnectionClosed += (WebSocket socket) => {
Connected = false;
Disconnected(this);
Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id);
};
}
private async Task handleMessage(string frame)
private async Task handleMessage(object sender, TextMessageEventArgs eventArgs)
{
// Update the 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>(eventArgs.Payload, "Event");
if(eventName == null) {
Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id);
@ -170,7 +160,7 @@ namespace Nibriboard.Client
Type jsonNet = typeof(JsonConvert);
MethodInfo deserialiserInfo = jsonNet.GetMethods().First(method => method.Name == "DeserializeObject" && method.IsGenericMethod);
MethodInfo genericInfo = deserialiserInfo.MakeGenericMethod(messageType);
var decodedMessage = genericInfo.Invoke(null, new object[] { frame });
var decodedMessage = genericInfo.Invoke(null, new object[] { eventArgs.Payload });
string handlerMethodName = "handle" + decodedMessage.GetType().Name;
Type clientType = this.GetType();
@ -180,11 +170,19 @@ namespace Nibriboard.Client
catch(Exception error)
{
Log.WriteLine("[NibriClient#{0}] Error decoding and / or handling message.", Id);
Log.WriteLine("[NibriClient#{0}] Raw frame content: {1}", Id, frame);
Log.WriteLine("[NibriClient#{0}] Raw frame content: {1}", Id, eventArgs.Payload);
Log.WriteLine("[NibriClient#{0}] Exception details: {1}", Id, error);
}
}
private Task handleDisconnection(object sender, ClientDisconnectedEventArgs eventArgs)
{
Disconnected?.Invoke(this);
Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id);
return Task.CompletedTask;
}
#endregion
@ -222,7 +220,7 @@ namespace Nibriboard.Client
Log.WriteLine("[NibriClient/#{0}] Sending message with length {1}.", Id, message.Length);
client.Send(message);
connection.Send(message);
return true;
}
@ -239,7 +237,7 @@ namespace Nibriboard.Client
/// <summary>
/// Closes the connection to the client gracefully.
/// </summary>
public void CloseConnection(Message lastMessage)
public async Task CloseConnection(Message lastMessage)
{
if (!Connected)
return;
@ -247,7 +245,7 @@ namespace Nibriboard.Client
// Tell the client that we're shutting down
Send(lastMessage);
client.Close();
await connection.Close(WebsocketCloseReason.Normal);
}
/// <summary>
@ -514,7 +512,7 @@ namespace Nibriboard.Client
protected ClientStatesMessage GenerateClientStateUpdate()
{
ClientStatesMessage result = new ClientStatesMessage();
foreach (NibriClient otherClient in manager.Clients)
foreach (NibriClient otherClient in manager.NibriClients)
{
// Don't include ourselves in the update message!
if (otherClient == this)

View file

@ -1,177 +0,0 @@
using System;
using IotWeb.Common.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Diagnostics;
using Nibriboard.Client.Messages;
using System.Threading;
using Nibriboard.RippleSpace;
namespace Nibriboard.Client
{
/// <summary>
/// Manages a group of <see cref="Nibriboard.Client.NibriClient"/>s.
/// </summary>
public class NibriClientManager : IWebSocketRequestHandler
{
/// <summary>
/// The ripple space manager that this client manager is connected to.
/// </summary>
public RippleSpaceManager SpaceManager { get; private set; }
private ClientSettings clientSettings;
public List<NibriClient> Clients = new List<NibriClient>();
public LineIncubator LineIncubator = new LineIncubator();
/// <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>
/// The number of clients currently connected to this Nibriboard.
/// </summary>
public int ClientCount {
get {
return Clients.Count;
}
}
public NibriClientManager(ClientSettings inClientSettings, RippleSpaceManager inSpaceManager, CancellationToken inCancellationToken)
{
clientSettings = inClientSettings;
canceller = inCancellationToken;
SpaceManager = inSpaceManager;
}
/// <summary>
/// Whether we will accept a given new WebSocket connection or not.
/// </summary>
/// <param name="uri">The uri the user connected to.</param>
/// <param name="protocol">The protocol the user is connecting with.</param>
/// <returns>Whether we want to accept the WebSocket connection attempt or not.</returns>
public bool WillAcceptRequest(string uri, string protocol)
{
//Log.WriteLine("[Nibriboard/Websocket] Accepting new {0} connection.", protocol);
return clientSettings.WebsocketProtocol == protocol;
}
/// <summary>
/// Handles WebSocket clients when they first connect, wrapping them in
/// a <see cref="Nibriboard.Client.NibriClient" /> instance and adding them to
/// the client list.
/// </summary>
/// <param name="newSocket">New socket.</param>
public void Connected(WebSocket newSocket)
{
NibriClient client = new NibriClient(this, newSocket);
client.Disconnected += handleDisconnection; // Clean up when the client disconnects
Clients.Add(client);
}
/// <summary>
/// Sends a message to all the connected clients, except the one who's sending it.
/// </summary>
/// <param name="sendingClient">The client sending the message.</param>
/// <param name="message">The message that is to bee sent.</param>
public void Broadcast(NibriClient sendingClient, Message message)
{
foreach(NibriClient client in Clients)
{
// Don't send the message to the sender
if (client == sendingClient)
continue;
client.Send(message);
}
}
/// <summary>
/// Sends a message to everyone on the same plane as the sender, except the sender themselves.
/// </summary>
/// <param name="sendingClient">The sending client.</param>
/// <param name="message">The message to send.</param>
public void BroadcastPlane(NibriClient sendingClient, Message message)
{
foreach(NibriClient client in Clients)
{
// Don't send the message to the sender
if(client == sendingClient)
continue;
// Only send the message to others on the same plane
if(client.CurrentPlane != sendingClient.CurrentPlane)
continue;
client.Send(message);
}
}
/// <summary>
/// Sends a message to everyone on a specified plane.
/// </summary>
/// <param name="plane">The plane to send the message to.</param>
/// <param name="message">The message to send.</param>
public void ReflectPlane(Plane plane, Message message)
{
foreach(NibriClient client in Clients)
{
if(client.CurrentPlane != plane)
continue;
client.Send(message);
}
}
/// <summary>
/// Periodically tidies up the client list, disconnecting old clients.
/// </summary>
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>
/// Clean up after a client disconnects from the server.
/// </summary>
/// <param name="disconnectedClient">The client that has disconnected.</param>
private void handleDisconnection(NibriClient disconnectedClient)
{
Clients.Remove(disconnectedClient);
}
}
}

View file

@ -69,11 +69,8 @@
<Compile Include="Utilities\EmbeddedFiles.cs" />
<Compile Include="Env.cs" />
<Compile Include="RippleSpace\RippleSpaceManager.cs" />
<Compile Include="Client\HttpEmbeddedFileHandler.cs" />
<Compile Include="Client\NibriClient.cs" />
<Compile Include="Client\NibriClientManager.cs" />
<Compile Include="Client\ClientSettings.cs" />
<Compile Include="Client\HttpClientSettingsHandler.cs" />
<Compile Include="Utilities\PointExtensions.cs" />
<Compile Include="Utilities\JsonUtilities.cs" />
<Compile Include="Client\Messages\Message.cs" />
@ -109,6 +106,7 @@
<Compile Include="RippleSpace\PlaneInfo.cs" />
<Compile Include="Utilities\BinaryIO.cs" />
<Compile Include="Client\Messages\ViewportUpdateMessage.cs" />
<Compile Include="NibriboardApp.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ClientFiles\index.html" />

202
Nibriboard/NibriboardApp.cs Normal file
View file

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Nibriboard.Client;
using Nibriboard.Client.Messages;
using Nibriboard.RippleSpace;
using SBRL.GlidingSquirrel.Http;
using SBRL.GlidingSquirrel.Websocket;
using SBRL.Utilities;
namespace Nibriboard
{
public class NibriboardAppStartInfo
{
public string FilePrefix { get; set; }
public ClientSettings ClientSettings { get; set; }
public RippleSpaceManager SpaceManager { get; set; }
}
public class NibriboardApp : WebsocketServer
{
private string filePrefix;
private List<string> embeddedFiles = new List<string>(EmbeddedFiles.ResourceList);
/// <summary>
/// The ripple space manager that this client manager is connected to.
/// </summary>
public RippleSpaceManager SpaceManager { get; private set; }
public LineIncubator LineIncubator = new LineIncubator();
private ClientSettings clientSettings;
public List<NibriClient> NibriClients = new List<NibriClient>();
/// <summary>
/// The number of clients currently connected to this Nibriboard.
/// </summary>
public int ClientCount {
get {
return Clients.Count;
}
}
public NibriboardApp(NibriboardAppStartInfo startInfo, IPAddress inBindAddress, int inPort) : base(inBindAddress, inPort)
{
clientSettings = startInfo.ClientSettings;
SpaceManager = startInfo.SpaceManager;
filePrefix = startInfo.FilePrefix;
MimeTypeOverrides.Add(".ico", "image/x-icon");
}
public override Task HandleClientConnected(object sender, ClientConnectedEventArgs eventArgs)
{
NibriClient client = new NibriClient(this, eventArgs.ConnectingClient);
client.Disconnected += (NibriClient disconnectedClient) => NibriClients.Remove(disconnectedClient);
NibriClients.Add(client);
return Task.CompletedTask;
}
public override Task HandleClientDisconnected(object sender, ClientDisconnectedEventArgs eventArgs)
{
return Task.CompletedTask;
}
public override async Task HandleHttpRequest(HttpRequest request, HttpResponse response)
{
if(request.Method != HttpMethod.GET)
{
response.ResponseCode = HttpResponseCode.MethodNotAllowed;
response.ContentType = "text/plain";
await response.SetBody("Error: That method isn't supported yet.");
logRequest(request, response);
return;
}
if(request.Url == "/Settings.json")
{
string settingsJson = JsonConvert.SerializeObject(clientSettings);
response.ContentLength = settingsJson.Length;
response.ContentType = "application/json";
await response.SetBody(settingsJson);
Log.WriteLine("[Http/ClientSettings] Sent settings to {0}", request.ClientAddress);
return;
}
string expandedFilePath = getEmbeddedFileReference(request.Url);
if(!embeddedFiles.Contains(expandedFilePath))
{
expandedFilePath += "index.html";
}
if(!embeddedFiles.Contains(expandedFilePath))
{
response.ResponseCode = HttpResponseCode.NotFound;
response.ContentType = "text/plain";
await response.SetBody($"Can't find expandedFilePath.");
logRequest(request, response);
return;
}
response.ContentType = LookupMimeType(expandedFilePath);
string embeddedFile = EmbeddedFiles.ReadAllText(expandedFilePath);
try
{
await response.SetBody(embeddedFile);
}
catch(Exception error)
{
Log.WriteLine($"[Nibriboard/EmbeddedFileHandler] Error: {error.Message} Details:");
Log.WriteLine(error.ToString());
}
logRequest(request, response);
}
#region Interface Methods
/// <summary>
/// Sends a message to all the connected clients, except the one who's sending it.
/// </summary>
/// <param name="sendingClient">The client sending the message.</param>
/// <param name="message">The message that is to bee sent.</param>
public void Broadcast(NibriClient sendingClient, Message message)
{
foreach(NibriClient client in NibriClients)
{
// Don't send the message to the sender
if(client == sendingClient)
continue;
client.Send(message);
}
}
/// <summary>
/// Sends a message to everyone on the same plane as the sender, except the sender themselves.
/// </summary>
/// <param name="sendingClient">The sending client.</param>
/// <param name="message">The message to send.</param>
public void BroadcastPlane(NibriClient sendingClient, Message message)
{
foreach(NibriClient client in NibriClients)
{
// Don't send the message to the sender
if(client == sendingClient)
continue;
// Only send the message to others on the same plane
if(client.CurrentPlane != sendingClient.CurrentPlane)
continue;
client.Send(message);
}
}
/// <summary>
/// Sends a message to everyone on a specified plane.
/// </summary>
/// <param name="plane">The plane to send the message to.</param>
/// <param name="message">The message to send.</param>
public void ReflectPlane(Plane plane, Message message)
{
foreach(NibriClient client in NibriClients)
{
if(client.CurrentPlane != plane)
continue;
client.Send(message);
}
}
#endregion
#region Utility Methods
protected string getEmbeddedFileReference(string uri)
{
return filePrefix + "." + uri.TrimStart("/".ToCharArray()).Replace('/', '.');
}
private void logRequest(HttpRequest request, HttpResponse response)
{
Log.WriteLine(
"[Http/FileHandler] {0} {1} {2} {3}",
response.ResponseCode,
response.ContentType,
request.Method,
request.Url
);
}
#endregion
}
}

View file

@ -1,14 +1,15 @@
using System;
using System.Threading.Tasks;
using System.Threading;
using Nibriboard.RippleSpace;
using Nibriboard.Client;
using System.Net.Sockets;
using System.Net;
using System.IO;
using SBRL.GlidingSquirrel.Websocket;
using Nibriboard.RippleSpace;
using Nibriboard.Client;
namespace Nibriboard
{
/// <summary>
@ -18,14 +19,11 @@ namespace Nibriboard
public class NibriboardServer
{
private TcpListener commandServer;
private WebsocketsServer httpServer;
private NibriboardApp appServer;
private ClientSettings clientSettings;
private RippleSpaceManager planeManager;
private readonly CancellationTokenSource clientManagerCanceller = new CancellationTokenSource();
private NibriClientManager clientManager;
public readonly int CommandPort = 31587;
public readonly int Port = 31586;
@ -49,37 +47,16 @@ namespace Nibriboard
};
// HTTP Server setup
httpServer = new HttpServer(Port);
httpServer.AddHttpRequestHandler(
"/",
new HttpEmbeddedFileHandler("Nibriboard.ClientFiles")
/*new HttpResourceHandler(
Assembly.GetExecutingAssembly(),
"ClientFiles",
"index.html"
)*/
);
httpServer.AddHttpRequestHandler(
"/Settings.json",
new HttpClientSettingsHandler(clientSettings)
);
// Websocket setup
clientManager = new NibriClientManager(
clientSettings,
planeManager,
clientManagerCanceller.Token
);
httpServer.AddWebSocketRequestHandler(
clientSettings.WebSocketPath,
clientManager
);
appServer = new NibriboardApp(new NibriboardAppStartInfo() {
FilePrefix = "Nibriboard.ClientFiles",
ClientSettings = clientSettings,
SpaceManager = planeManager
}, IPAddress.IPv6Any, Port);
}
public async Task Start()
{
httpServer.Start();
await appServer.Start();
Log.WriteLine("[NibriboardServer] Started on port {0}", Port);
await planeManager.StartMaintenanceMonkey();

@ -1 +1 @@
Subproject commit e219ad112d256e00563eca76f13691ebe486e3bd
Subproject commit e59dd72dd475b685ede1a05258ea0369bdcbc8c1