From 2f58fc06710b87f9fc0dd9ac2e3110c703761870 Mon Sep 17 00:00:00 2001 From: Michael Biggins Date: Mon, 28 Sep 2020 16:29:21 +0100 Subject: [PATCH] Add an alternative NodeLocator that doesn't rely on Redis for smaller multi-server setups with a single Master. --- .gitignore | 1 + .../Data/ServerRedirectorConfig.cs | 2 + .../Net/Redirector/NodeLocatorUDPSockets.cs | 136 ++++++++++++++++++ src/Impostor.Server/Program.cs | 23 +-- src/Impostor.Server/config.json | 2 + 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs diff --git a/.gitignore b/.gitignore index 2da2c06..33eefb4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ ################################################################################ /.vscode/tasks.json +/.vscode/launch.json diff --git a/src/Impostor.Server/Data/ServerRedirectorConfig.cs b/src/Impostor.Server/Data/ServerRedirectorConfig.cs index cd5b4d7..188ea78 100644 --- a/src/Impostor.Server/Data/ServerRedirectorConfig.cs +++ b/src/Impostor.Server/Data/ServerRedirectorConfig.cs @@ -8,7 +8,9 @@ namespace Impostor.Server.Data public bool Enabled { get; set; } public bool Master { get; set; } + public bool UseRedis { get; set; } public string Redis { get; set; } + public string UDPMasterEndpoint { get; set; } public List Nodes { get; set; } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs new file mode 100644 index 0000000..8f07e20 --- /dev/null +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs @@ -0,0 +1,136 @@ +using Impostor.Server.Data; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUDPSockets : INodeLocator, IDisposable + { + readonly UdpClient client; + private bool disposedValue; + + private class AvailableNode { + public IPEndPoint Endpoint { get; set; } + public DateTime LastUpdated { get; set; } + public bool Expired => LastUpdated < DateTime.Now.AddHours(-1); + } + + private Dictionary AvailableNodes; + + internal NodeLocatorUDPSockets(IOptions config) + { + if (!Uri.TryCreate(config.Value.UDPMasterEndpoint, UriKind.Absolute, out Uri ServerAddress) || ServerAddress.Scheme.ToLower() != "udp") + { + throw new Exception($"UDPMasterEndpoint has an invalid value of '{config.Value.UDPMasterEndpoint}'. Expected udp://ip:port/ schema."); + } + + if (config.Value.Master) + { + if (!IPAddress.TryParse(ServerAddress.Host, out var LocalIPBinding)) { + throw new Exception("$UDPMasterEndpoint must use an IP address rather than a hostname when acting as a master."); + } + + client = new UdpClient(new IPEndPoint(LocalIPBinding, ServerAddress.Port)); + AvailableNodes = new Dictionary(); + Task.Run(HandleClientPackets); + } + else + { + client = new UdpClient(ServerAddress.Host, ServerAddress.Port); + } + } + + public IPEndPoint Find(string gameCode) + { + lock (AvailableNodes) + { + return AvailableNodes.ContainsKey(gameCode) ? AvailableNodes[gameCode].Endpoint : null; + } + } + + private void HandleClientPackets() + { + try + { + while (true) + { + var data = client.ReceiveAsync().GetAwaiter().GetResult(); + //TODO: Log that we got an update from the given remote EP. + string message = Encoding.UTF8.GetString(data.Buffer); + var parts = message.Split(',', 2); + if (parts.Length != 2) { continue; } + + if (!IPEndPoint.TryParse(parts[1], out var Endpoint)) { continue; } + + HandleUpdate(parts[0], Endpoint); + } + } + catch (ObjectDisposedException) + { + return; //Bail out and don't try anything else. + } + catch + { + //Something else went wrong but we're not shutting down, give up and try again. + if (!disposedValue) { Task.Run(HandleClientPackets); } + } + } + + private void HandleUpdate(string gameCode, IPEndPoint endpoint) + { + lock (AvailableNodes) + { + var node = AvailableNodes.ContainsKey(gameCode) ? AvailableNodes[gameCode] : new AvailableNode(); + node.Endpoint = endpoint; + node.LastUpdated = DateTime.Now; + AvailableNodes[gameCode] = node; + + var KeysToRemove = AvailableNodes.Where(kvp => kvp.Value.Expired).Select(kvp => kvp.Key).ToList(); + KeysToRemove.ForEach(n => AvailableNodes.Remove(n)); + } + } + + public void Remove(string gameCode) + { + lock (AvailableNodes) + { + AvailableNodes.Remove(gameCode); + } + } + + public void Save(string gameCode, IPEndPoint endPoint) + { + byte[] data = Encoding.UTF8.GetBytes($"{endPoint},{gameCode}"); + client.Send(data, data.Length); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + client.Close(); + client.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 7a665f1..03e80c7 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -66,18 +66,25 @@ namespace Impostor.Server { // When joining a game, it retrieves the game server ip from redis. // When a game has been created on this node, it stores the game code with its ip in redis. - services.AddSingleton(); + if (redirector.UseRedis) + { + services.AddSingleton(); + + // Dependency for the NodeLocatorRedis. + services.AddStackExchangeRedisCache(options => + { + options.Configuration = redirector.Redis; + options.InstanceName = "ImpostorRedis"; + }); + } + else + { + services.AddSingleton(); + } // Use the configuration as source for the list of nodes to provide // when creating a game. services.AddSingleton(); - - // Dependency for the NodeLocatorRedis. - services.AddStackExchangeRedisCache(options => - { - options.Configuration = redirector.Redis; - options.InstanceName = "ImpostorRedis"; - }); } else { diff --git a/src/Impostor.Server/config.json b/src/Impostor.Server/config.json index 6c8f49a..dd8b41f 100644 --- a/src/Impostor.Server/config.json +++ b/src/Impostor.Server/config.json @@ -8,7 +8,9 @@ "ServerRedirector": { "Enabled": false, "Master": true, + "UseRedis": true, "Redis": "127.0.0.1:6379", + "UDPMasterEndpoint": "127.0.0.1:32320", "Nodes": [ { "Ip": "127.0.0.1", -- 2.39.5