-################################################################################
-# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
-################################################################################
-
-/.vscode/tasks.json
-/.vscode/launch.json
+/.vscode
\ No newline at end of file
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
using System;
using System.Net;
using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
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;
}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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
+++ /dev/null
-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);
- }
- }
-}
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
}
services.AddSingleton<Matchmaker>();
-
services.AddHostedService<MatchmakerService>();
})
+ .UseConsoleLifetime()
.UseSerilog();
}
}
\ No newline at end of file
"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",