]> git.deb.at Git - rhonda/impostor.git/commitdiff
Use a BackgroundService for receiving node updates
authorAeonLucid <aeonlucid@gmail.com>
Wed, 30 Sep 2020 19:53:42 +0000 (21:53 +0200)
committerAeonLucid <aeonlucid@gmail.com>
Wed, 30 Sep 2020 19:53:42 +0000 (21:53 +0200)
.gitignore
src/Impostor.Server/Data/ServerRedirectorConfig.cs
src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs
src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs [new file with mode: 0644]
src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs [new file with mode: 0644]
src/Impostor.Server/Net/Redirector/NodeLocatorUDPSockets.cs [deleted file]
src/Impostor.Server/Program.cs
src/Impostor.Server/config.json

index 33eefb43e466a20e30937b2a28ef810f546ff094..fbb234620980d2addac5bbac6500144c886838ae 100644 (file)
@@ -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
index 1424df6d53cca1b79c44c35eee2dd2898518ab8c..d2ebe518c41c60ad5d201f300f19075032632377 100644 (file)
@@ -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<ServerRedirectorNode> Nodes { get; set; }
+
+        public class NodeLocator
+        {
+            public string Redis { get; set; }
+            public string UDPMasterEndpoint { get; set; }
+        }
     }
 }
\ No newline at end of file
index 1819eac6d666295473d7f02688ad5a4444e896a5..e611887879883cf4a85eb05a25814dd3095330c6 100644 (file)
@@ -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<NodeLocatorRedis> 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 (file)
index 0000000..7b32ed5
--- /dev/null
@@ -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<NodeLocatorUDP> _logger;
+        private readonly bool _isMaster;
+        private readonly IPEndPoint _server;
+        private readonly UdpClient _client;
+        private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes;
+        
+        public NodeLocatorUDP(ILogger<NodeLocatorUDP> logger, IOptions<ServerRedirectorConfig> config)
+        {
+            _logger = logger;
+            
+            if (config.Value.Master)
+            {
+                _isMaster = true;
+                _availableNodes = new ConcurrentDictionary<string, AvailableNode>();
+            }
+            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 (file)
index 0000000..eeb5e38
--- /dev/null
@@ -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<NodeLocatorUDPService> _logger;
+        private readonly UdpClient _client;
+
+        public NodeLocatorUDPService(
+            INodeLocator nodeLocator, 
+            ILogger<NodeLocatorUDPService> logger, 
+            IOptions<ServerRedirectorConfig> 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 (file)
index eec9a44..0000000
+++ /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<string, AvailableNode> AvailableNodes;
-
-        public NodeLocatorUDPSockets(IOptions<ServerRedirectorConfig> 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<string, AvailableNode>();
-                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);
-        }
-    }
-}
index 03e80c7da67b158888f2e119de65bc863e8180aa..96dd162a913922683eafbf48741de33ffbdfe745 100644 (file)
@@ -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<INodeLocator, NodeLocatorRedis>();
 
                             // 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<INodeLocator, NodeLocatorUDP>();
+
+                            if (redirector.Master)
+                            {
+                                services.AddHostedService<NodeLocatorUDPService>();
+                            }
+                        }
                         else
                         {
-                            services.AddSingleton<INodeLocator, NodeLocatorUDPSockets>();
+                            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<Matchmaker>();
-                    
                     services.AddHostedService<MatchmakerService>();
                 })
+                .UseConsoleLifetime()
                 .UseSerilog();
     }
 }
\ No newline at end of file
index 74f042aaeb0c08c027bfec4b7ecfdd1dc7994bb4..d7b604eafed2dc986fd12a624fe1692f4324ce4d 100644 (file)
@@ -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",