Use a BackgroundService for receiving node updates

This commit is contained in:
AeonLucid committed 2020-09-30 21:53:42 +02:00
1 parent daa1f8ee02
commit 3be4088844
8 files changed
+246 -155

No files matched your search

+1 -6
View File
@@ -1,6 +1 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
/.vscode/tasks.json
/.vscode/launch.json
/.vscode
@@ -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; }
}
}
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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.");
}
}
}
@@ -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);
}
}
}
+15 -6
View 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();
}
}
+4 -3
View 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",