Skip to content

Commit

Permalink
Setup heartbeat and connection monitoring (#3)
Browse files Browse the repository at this point in the history
* Add heartbeat
Fix disconnet message

* Setup connection monitoring for proactive termination
  • Loading branch information
AlexMacocian authored Nov 24, 2023
1 parent 34da14a commit 656d93f
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 5 deletions.
4 changes: 3 additions & 1 deletion GuildWarsPartySearch/Config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"ServerOptions": {
"ApiKey": "[API_KEY_PLACEHOLDER]"
"ApiKey": "[API_KEY_PLACEHOLDER]",
"InactivityTimeout": "0:0:5",
"HeartbeatFrequency": "0:0:1"
}
}
2 changes: 1 addition & 1 deletion GuildWarsPartySearch/Endpoints/LiveFeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
10 changes: 8 additions & 2 deletions GuildWarsPartySearch/Launch/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<IOptions<ServerOptions>>().Value.HeartbeatFrequency ?? TimeSpan.FromSeconds(5)))
.AddHandler(new ConnectionMonitorHandler())
.AddServerUsageMonitor(new TickrateEnforcer() { TicksPerSecond = 60, Silent = true })
.SetScheduler(new TaskAwaiterScheduler())
.WithLoggingMessageContents(false);

Expand Down
4 changes: 3 additions & 1 deletion GuildWarsPartySearch/Launch/ServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ public static WebsocketRoutingHandler SetupRoutes(this WebsocketRoutingHandler w
websocketRoutingHandler.ThrowIfNull();
websocketRoutingHandler
.AddRoute<LiveFeed>("party-search/live-feed")
.AddRoute<PostPartySearch>("party-search/update", FilterUpdateMessages);
.AddRoute<PostPartySearch>("party-search/update", FilterUpdateMessages)
.WithHeartbeatEnabled(true)
.WithHeartbeatFrequency(TimeSpan.FromSeconds(5));
return websocketRoutingHandler;
}

Expand Down
6 changes: 6 additions & 0 deletions GuildWarsPartySearch/Options/ServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
88 changes: 88 additions & 0 deletions GuildWarsPartySearch/Tcp/ConnectionMonitorHandler.cs
Original file line number Diff line number Diff line change
@@ -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<IOptions<ServerOptions>>().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;
}
}
}

0 comments on commit 656d93f

Please sign in to comment.