From: AeonLucid Date: Wed, 30 Sep 2020 19:53:42 +0000 (+0200) Subject: Use a BackgroundService for receiving node updates X-Git-Tag: v1.1.0~1^2~22^2 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=3be40888448a897702d43b5d46a44fc3124bf2f7;p=rhonda%2Fimpostor.git Use a BackgroundService for receiving node updates --- diff --git a/.gitignore b/.gitignore index 33eefb4..fbb2346 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ -################################################################################ -# This .gitignore file was automatically created by Microsoft(R) Visual Studio. -################################################################################ - -/.vscode/tasks.json -/.vscode/launch.json +/.vscode \ No newline at end of file diff --git a/src/Impostor.Server/Data/ServerRedirectorConfig.cs b/src/Impostor.Server/Data/ServerRedirectorConfig.cs index 1424df6..d2ebe51 100644 --- a/src/Impostor.Server/Data/ServerRedirectorConfig.cs +++ b/src/Impostor.Server/Data/ServerRedirectorConfig.cs @@ -8,9 +8,13 @@ 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 NodeLocator Locator { get; set; } public List Nodes { get; set; } + + public class NodeLocator + { + public string Redis { get; set; } + public string UDPMasterEndpoint { get; set; } + } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs index 1819eac..e611887 100644 --- a/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs @@ -1,6 +1,7 @@ using System; using System.Net; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Redirector { @@ -8,8 +9,9 @@ namespace Impostor.Server.Net.Redirector { private readonly IDistributedCache _cache; - public NodeLocatorRedis(IDistributedCache cache) + public NodeLocatorRedis(ILogger logger, IDistributedCache cache) { + logger.LogWarning("Using the redis NodeLocator."); _cache = cache; } diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs new file mode 100644 index 0000000..7b32ed5 --- /dev/null +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs @@ -0,0 +1,120 @@ +using Impostor.Server.Data; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUDP : INodeLocator, IDisposable + { + private readonly ILogger _logger; + private readonly bool _isMaster; + private readonly IPEndPoint _server; + private readonly UdpClient _client; + private readonly ConcurrentDictionary _availableNodes; + + public NodeLocatorUDP(ILogger logger, IOptions config) + { + _logger = logger; + + if (config.Value.Master) + { + _isMaster = true; + _availableNodes = new ConcurrentDictionary(); + } + else + { + _isMaster = false; + + if (!IPEndPoint.TryParse(config.Value.Locator.UDPMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UDPMasterEndpoint should be in the ip:port format."); + } + + _logger.LogWarning("Node server will send updates to {0}.", endpoint); + _server = endpoint; + _client = new UdpClient + { + DontFragment = true + }; + } + } + + public void Update(IPEndPoint ip, string gameCode) + { + _logger.LogDebug("Received update {0} -> {1}", gameCode, ip); + + _availableNodes.AddOrUpdate(gameCode, s => new AvailableNode + { + Endpoint = ip, + LastUpdated = DateTimeOffset.UtcNow + }, (s, node) => + { + node.Endpoint = ip; + node.LastUpdated = DateTimeOffset.UtcNow; + + return node; + }); + + foreach (var (key, value) in _availableNodes) + { + if (value.Expired) + { + _availableNodes.TryRemove(key, out _); + } + } + } + + public IPEndPoint Find(string gameCode) + { + if (!_isMaster) + { + return null; + } + + if (_availableNodes.TryGetValue(gameCode, out var node)) + { + if (node.Expired) + { + _availableNodes.TryRemove(gameCode, out _); + return null; + } + + return node.Endpoint; + } + + return null; + } + + public void Remove(string gameCode) + { + if (!_isMaster) + { + return; + } + + _availableNodes.TryRemove(gameCode, out _); + } + + public void Save(string gameCode, IPEndPoint endPoint) + { + var data = Encoding.UTF8.GetBytes($"{gameCode},{endPoint}"); + _client.Send(data, data.Length, _server); + } + + public void Dispose() + { + _client?.Dispose(); + } + + private class AvailableNode { + public IPEndPoint Endpoint { get; set; } + public DateTimeOffset LastUpdated { get; set; } + public bool Expired => LastUpdated < DateTimeOffset.UtcNow.AddHours(-1); + } + } +} diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs new file mode 100644 index 0000000..eeb5e38 --- /dev/null +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs @@ -0,0 +1,96 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUDPService : BackgroundService + { + private readonly NodeLocatorUDP _nodeLocator; + private readonly ILogger _logger; + private readonly UdpClient _client; + + public NodeLocatorUDPService( + INodeLocator nodeLocator, + ILogger logger, + IOptions options) + { + _nodeLocator = (NodeLocatorUDP) nodeLocator; + _logger = logger; + + if (!IPEndPoint.TryParse(options.Value.Locator.UDPMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UDPMasterEndpoint should be in the ip:port format."); + } + + _client = new UdpClient(endpoint) + { + DontFragment = true + }; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogWarning("Master server is listening for node updates on {0}.", _client.Client.LocalEndPoint); + + stoppingToken.Register(() => + { + _client.Close(); + _client.Dispose(); + }); + + try + { + while (!stoppingToken.IsCancellationRequested) + { + // Receive data from a node. + UdpReceiveResult data; + + try + { + data = await _client.ReceiveAsync(); + } + catch (ObjectDisposedException) + { + break; + } + + // Check if data is valid. + if (data.Buffer.Length == 0) + { + break; + } + + // Parse the data. + var message = Encoding.UTF8.GetString(data.Buffer); + var parts = message.Split(',', 2); + if (parts.Length != 2) + { + continue; + } + + if (!IPEndPoint.TryParse(parts[1], out var ipEndPoint)) + { + continue; + } + + // Update the NodeLocator. + _nodeLocator.Update(ipEndPoint, parts[0]); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error in NodeLocatorUDPService."); + } + + _logger.LogWarning("Master server node update listener is stopping."); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs deleted file mode 100644 index eec9a44..0000000 --- a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs +++ /dev/null @@ -1,136 +0,0 @@ -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; - - public 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 03e80c7..96dd162 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -64,22 +64,31 @@ namespace Impostor.Server if (redirector.Enabled) { - // 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. - if (redirector.UseRedis) + if (!string.IsNullOrEmpty(redirector.Locator.Redis)) { + // 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(); // Dependency for the NodeLocatorRedis. services.AddStackExchangeRedisCache(options => { - options.Configuration = redirector.Redis; + options.Configuration = redirector.Locator.Redis; options.InstanceName = "ImpostorRedis"; }); } + else if (!string.IsNullOrEmpty(redirector.Locator.UDPMasterEndpoint)) + { + services.AddSingleton(); + + if (redirector.Master) + { + services.AddHostedService(); + } + } else { - services.AddSingleton(); + throw new Exception("Missing a valid NodeLocator config."); } // Use the configuration as source for the list of nodes to provide @@ -105,9 +114,9 @@ namespace Impostor.Server } services.AddSingleton(); - services.AddHostedService(); }) + .UseConsoleLifetime() .UseSerilog(); } } \ No newline at end of file diff --git a/src/Impostor.Server/config.json b/src/Impostor.Server/config.json index 74f042a..d7b604e 100644 --- a/src/Impostor.Server/config.json +++ b/src/Impostor.Server/config.json @@ -8,9 +8,10 @@ "ServerRedirector": { "Enabled": false, "Master": true, - "UseRedis": true, - "Redis": "127.0.0.1:6379", - "UDPMasterEndpoint": "udp://127.0.0.1:32320", + "Locator": { + "Redis": "127.0.0.1.6379", + "UDPMasterEndpoint": "127.0.0.1:32320" + }, "Nodes": [ { "Ip": "127.0.0.1",