mirror of
https://github.com/Impostor/Impostor.git
synced 2026-10-19 15:51:42 +00:00
Use a BackgroundService for receiving node updates
This commit is contained in:
1 parent
daa1f8ee02
commit
3be4088844
8 files changed
+246
-155
No files matched your search
+1
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user