From 656d93f42e7b6766650df317e2eb1364ef832c2b Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Sat, 25 Nov 2023 00:09:10 +0100 Subject: [PATCH] Setup heartbeat and connection monitoring (#3) * Add heartbeat Fix disconnet message * Setup connection monitoring for proactive termination --- GuildWarsPartySearch/Config.json | 4 +- GuildWarsPartySearch/Endpoints/LiveFeed.cs | 2 +- GuildWarsPartySearch/Launch/Program.cs | 10 ++- .../Launch/ServerConfiguration.cs | 4 +- GuildWarsPartySearch/Options/ServerOptions.cs | 6 ++ .../Tcp/ConnectionMonitorHandler.cs | 88 +++++++++++++++++++ 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 GuildWarsPartySearch/Tcp/ConnectionMonitorHandler.cs diff --git a/GuildWarsPartySearch/Config.json b/GuildWarsPartySearch/Config.json index 17c15b2..7ac09ab 100644 --- a/GuildWarsPartySearch/Config.json +++ b/GuildWarsPartySearch/Config.json @@ -1,5 +1,7 @@ { "ServerOptions": { - "ApiKey": "[API_KEY_PLACEHOLDER]" + "ApiKey": "[API_KEY_PLACEHOLDER]", + "InactivityTimeout": "0:0:5", + "HeartbeatFrequency": "0:0:1" } } \ No newline at end of file diff --git a/GuildWarsPartySearch/Endpoints/LiveFeed.cs b/GuildWarsPartySearch/Endpoints/LiveFeed.cs index e6061b3..e2edca6 100644 --- a/GuildWarsPartySearch/Endpoints/LiveFeed.cs +++ b/GuildWarsPartySearch/Endpoints/LiveFeed.cs @@ -30,7 +30,7 @@ public LiveFeed( public override void ConnectionClosed() { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ConnectionInitialized), this.ClientData.Socket.RemoteEndPoint?.ToString() ?? string.Empty); - scopedLogger.LogInformation("Client connected"); + scopedLogger.LogInformation("Client disconnected"); this.liveFeedService.RemoveClient(this.ClientData); } diff --git a/GuildWarsPartySearch/Launch/Program.cs b/GuildWarsPartySearch/Launch/Program.cs index 5dab3be..0cbf27e 100644 --- a/GuildWarsPartySearch/Launch/Program.cs +++ b/GuildWarsPartySearch/Launch/Program.cs @@ -1,4 +1,8 @@ // See https://aka.ms/new-console-template for more information +using GuildWarsPartySearch.Server.Options; +using GuildWarsPartySearch.Server.Tcp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using MTSC.ServerSide.Handlers; using MTSC.ServerSide.Schedulers; using MTSC.ServerSide.UsageMonitors; @@ -18,8 +22,10 @@ private static async Task Main() .AddHandler( new WebsocketRoutingHandler() .SetupRoutes() - .WithHeartbeatEnabled(true)) - .AddServerUsageMonitor(new TickrateEnforcer() { TicksPerSecond = 240, Silent = true }) + .WithHeartbeatEnabled(true) + .WithHeartbeatFrequency(server.ServiceManager.GetRequiredService>().Value.HeartbeatFrequency ?? TimeSpan.FromSeconds(5))) + .AddHandler(new ConnectionMonitorHandler()) + .AddServerUsageMonitor(new TickrateEnforcer() { TicksPerSecond = 60, Silent = true }) .SetScheduler(new TaskAwaiterScheduler()) .WithLoggingMessageContents(false); diff --git a/GuildWarsPartySearch/Launch/ServerConfiguration.cs b/GuildWarsPartySearch/Launch/ServerConfiguration.cs index 1881ef4..b2ab406 100644 --- a/GuildWarsPartySearch/Launch/ServerConfiguration.cs +++ b/GuildWarsPartySearch/Launch/ServerConfiguration.cs @@ -52,7 +52,9 @@ public static WebsocketRoutingHandler SetupRoutes(this WebsocketRoutingHandler w websocketRoutingHandler.ThrowIfNull(); websocketRoutingHandler .AddRoute("party-search/live-feed") - .AddRoute("party-search/update", FilterUpdateMessages); + .AddRoute("party-search/update", FilterUpdateMessages) + .WithHeartbeatEnabled(true) + .WithHeartbeatFrequency(TimeSpan.FromSeconds(5)); return websocketRoutingHandler; } diff --git a/GuildWarsPartySearch/Options/ServerOptions.cs b/GuildWarsPartySearch/Options/ServerOptions.cs index cfeb59e..eec65ff 100644 --- a/GuildWarsPartySearch/Options/ServerOptions.cs +++ b/GuildWarsPartySearch/Options/ServerOptions.cs @@ -6,4 +6,10 @@ public sealed class ServerOptions { [JsonProperty(nameof(ApiKey))] public string? ApiKey { get; set; } + + [JsonProperty(nameof(InactivityTimeout))] + public TimeSpan? InactivityTimeout { get; set; } + + [JsonProperty(nameof(HeartbeatFrequency))] + public TimeSpan? HeartbeatFrequency { get; set; } } diff --git a/GuildWarsPartySearch/Tcp/ConnectionMonitorHandler.cs b/GuildWarsPartySearch/Tcp/ConnectionMonitorHandler.cs new file mode 100644 index 0000000..ef15e6b --- /dev/null +++ b/GuildWarsPartySearch/Tcp/ConnectionMonitorHandler.cs @@ -0,0 +1,88 @@ +using MTSC.ServerSide; +using MTSC; +using System.Net.Sockets; +using MTSC.ServerSide.Handlers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using GuildWarsPartySearch.Server.Options; + +namespace GuildWarsPartySearch.Server.Tcp; + +public class ConnectionMonitorHandler : IHandler +{ + private bool initialized; + private TimeSpan inactivityTimeout; + + void IHandler.ClientRemoved(MTSC.ServerSide.Server server, ClientData client) { } + + bool IHandler.HandleClient(MTSC.ServerSide.Server server, ClientData client) => false; + + bool IHandler.HandleReceivedMessage(MTSC.ServerSide.Server server, ClientData client, Message message) => false; + + bool IHandler.HandleSendMessage(MTSC.ServerSide.Server server, ClientData client, ref Message message) => false; + + bool IHandler.PreHandleReceivedMessage(MTSC.ServerSide.Server server, ClientData client, ref Message message) => false; + + void IHandler.Tick(MTSC.ServerSide.Server server) + { + if (!this.initialized) + { + this.initialized = true; + this.inactivityTimeout = server.ServiceManager.GetRequiredService>().Value.InactivityTimeout ?? TimeSpan.FromSeconds(15); + } + + foreach (ClientData client in server.Clients) + { + if ((DateTime.Now - client.LastActivityTime) > this.inactivityTimeout) + { + if (!IsConnected(client.Socket)) + { + server.Log("Disconnected: " + client.Socket.RemoteEndPoint?.ToString()); + client.ToBeRemoved = true; + } + } + } + } + + private bool IsConnected(Socket client) + { + try + { + if (client is not null && client.Connected) + { + /* pear to the documentation on Poll: + * When passing SelectMode.SelectRead as a parameter to the Poll method it will return + * -either- true if Socket.Listen(Int32) has been called and a connection is pending; + * -or- true if data is available for reading; + * -or- true if the connection has been closed, reset, or terminated; + * otherwise, returns false + */ + + // Detect if client disconnected + if (client.Poll(0, SelectMode.SelectRead)) + { + byte[] buff = new byte[1]; + if (client.Receive(buff, SocketFlags.Peek) == 0) + { + // Client disconnected + return false; + } + else + { + return true; + } + } + + return true; + } + else + { + return false; + } + } + catch + { + return false; + } + } +}