Added server redirection using redis

This commit is contained in:
AeonLucid committed 2020-09-26 05:43:37 +02:00
1 parent 7b6c55c78e
commit f2a23d5bbb
27 files changed
+451 -61

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.301
dotnet-version: 3.1.402
- name: Install dependencies
run: dotnet restore ./src
- name: Build Impostor.Client
+1 -11
View File
@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.301
dotnet-version: 3.1.402
- name: Install dependencies
run: dotnet restore ./src
- name: Build Impostor.Client
@@ -35,13 +35,3 @@ jobs:
run: dotnet build --no-restore -c Release -f netcoreapp3.1 ./src/Impostor.Server/Impostor.Server.csproj /p:PublishTrimmed=false
- name: Test
run: dotnet test --no-restore -v normal -f netcoreapp3.1 ./src
deploy:
name: Deploy
runs-on: windows-latest
needs: build
if: github.event_name == 'release'
steps:
- name: Publish Impostor.Server (win-x64)
run: dotnet publish -c release -o /build/linux-x64 -f netcoreapp3.1 -r win-x64 --self-contained --no-restore ./src/Impostor.Server/Impostor.Server.csproj
- name: Publish Impostor.Server (linux-x64)
run: dotnet publish -c release -o /build/linux-x64 -f netcoreapp3.1 -r linux-x64 --self-contained --no-restore ./src/Impostor.Server/Impostor.Server.csproj
+2
View File
@@ -1,3 +1,5 @@
config.*.json
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
@@ -8,5 +8,8 @@
public const string Destroyed = "The game you tried to join is being destroyed. " +
"Please create a new game.";
public const string NotImplemented = "Game listing has not been implemented in Impostor yet for servers " +
"running in server redirection mode.";
}
}
+3 -1
View File
@@ -1,7 +1,9 @@
namespace Impostor.Server.Data
{
public class ServerConfig
internal class ServerConfig
{
public const string Section = "Server";
public string PublicIp { get; set; } = "127.0.0.1";
public ushort PublicPort { get; set; } = 22023;
public string ListenIp { get; set; } = "127.0.0.1";
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Impostor.Server.Data
{
internal class ServerRedirectorConfig
{
public const string Section = "ServerRedirector";
public bool Enabled { get; set; }
public bool Master { get; set; }
public string Redis { get; set; }
public List<ServerRedirectorNode> Nodes { get; set; }
}
}
@@ -0,0 +1,8 @@
namespace Impostor.Server.Data
{
internal class ServerRedirectorNode
{
public string Ip { get; set; }
public ushort Port { get; set; }
}
}
@@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.8" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
@@ -27,6 +28,9 @@
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="config.*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+2 -2
View File
@@ -1,4 +1,4 @@
using System;
using System;
using Hazel;
using Impostor.Server.Data;
using Impostor.Server.Net.Manager;
@@ -10,7 +10,7 @@ using ILogger = Serilog.ILogger;
namespace Impostor.Server.Net
{
public class Client
internal class Client
{
private static readonly ILogger Logger = Log.ForContext<Client>();
@@ -1,26 +1,29 @@
using System.Collections.Concurrent;
using Hazel;
using Impostor.Server.Exceptions;
using Serilog;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Manager
{
public class ClientManager
internal class ClientManager : IClientManager
{
private static readonly ILogger Logger = Log.ForContext<ClientManager>();
private readonly ILogger<ClientManager> _clientManager;
private readonly GameManager _gameManager;
private readonly ConcurrentDictionary<int, Client> _clients;
private readonly object _idLock;
private int _idLast;
public ClientManager()
public ClientManager(ILogger<ClientManager> clientManager, GameManager gameManager)
{
_clientManager = clientManager;
_gameManager = gameManager;
_clients = new ConcurrentDictionary<int, Client>();
_idLock = new object();
_idLast = 0;
}
// No idea what a good way for this is.
public int NextId()
private int NextId()
{
lock (_idLock)
{
@@ -35,7 +38,7 @@ namespace Impostor.Server.Net.Manager
_idLast = 0;
}
if (_clients.ContainsKey(_idLast))
if (_clients.ContainsKey(result))
{
continue;
}
@@ -47,17 +50,17 @@ namespace Impostor.Server.Net.Manager
}
}
public void Add(Client client)
public void Create(string name, Connection connection)
{
Logger.Information("Client connected.");
_clients.TryAdd(client.Id, client);
var clientId = NextId();
_clientManager.LogInformation("Client connected.");
_clients.TryAdd(clientId, new Client(this, _gameManager, clientId, name, connection));
}
public void Remove(Client client)
{
Logger.Information("Client disconnected.");
_clientManager.LogInformation("Client disconnected.");
_clients.TryRemove(client.Id, out _);
}
}
+16 -7
View File
@@ -1,8 +1,9 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Impostor.Server.Data;
using Impostor.Server.Net.Redirector;
using Impostor.Server.Net.State;
using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
@@ -11,26 +12,33 @@ using Microsoft.Extensions.Options;
namespace Impostor.Server.Net.Manager
{
public class GameManager
internal class GameManager
{
private readonly ILogger<GameManager> _logger;
private readonly INodeProvider _nodeProvider;
private readonly IPEndPoint _publicIp;
private readonly ConcurrentDictionary<int, Game> _games;
public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config)
public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeProvider nodeProvider)
{
_logger = logger;
_nodeProvider = nodeProvider;
_publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort);
_games = new ConcurrentDictionary<int, Game>();
}
public Game Create(GameOptionsData options)
{
// TODO: Prevent duplicates when using server redirector using INodeProvider.
var gameCode = GameCode.GenerateCode(6);
var game = new Game(this, _publicIp, gameCode, options);
var gameCodeStr = GameCode.IntToGameName(gameCode);
var game = new Game(this, _nodeProvider, _publicIp, gameCode, options);
if (_games.TryAdd(gameCode, game))
if (_nodeProvider.Find(gameCodeStr) == null &&
_games.TryAdd(gameCode, game))
{
_nodeProvider.Save(gameCodeStr, _publicIp);
_logger.LogDebug("Created game with code {0} ({1}).", game.CodeStr, gameCode);
return game;
}
@@ -53,7 +61,7 @@ namespace Impostor.Server.Net.Manager
foreach (var (code, game) in _games.Where(x =>
x.Value.IsPublic &&
x.Value.GameState == GameStates.NotStarted &&
x.Value.PlayerCount < 10))
x.Value.PlayerCount < 10)) // TODO: Do "< x.Value.Options.MaxPlayers" when GameData packets are done.
{
// Check for options.
// TODO: Re-enable map filter when GameData packets are done.
@@ -77,7 +85,8 @@ namespace Impostor.Server.Net.Manager
public void Remove(int gameCode)
{
_logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode);
_logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode);
_nodeProvider.Remove(GameCode.IntToGameName(gameCode));
_games.TryRemove(gameCode, out _);
}
}
@@ -0,0 +1,9 @@
using Hazel;
namespace Impostor.Server.Net.Manager
{
internal interface IClientManager
{
void Create(string name, Connection connection);
}
}
+12 -11
View File
@@ -10,21 +10,22 @@ using Microsoft.Extensions.Options;
namespace Impostor.Server.Net
{
public class Matchmaker
internal class Matchmaker
{
private readonly ILogger<Matchmaker> _logger;
private readonly ServerConfig _config;
private readonly GameManager _gameManager;
private readonly ClientManager _clientManager;
private readonly ServerConfig _serverConfig;
private readonly IClientManager _clientManager;
private readonly UdpConnectionListener _connection;
public Matchmaker(ILogger<Matchmaker> logger, IOptions<ServerConfig> configOptions, GameManager gameManager)
public Matchmaker(
ILogger<Matchmaker> logger,
IOptions<ServerConfig> serverConfig,
IClientManager clientManager)
{
_logger = logger;
_config = configOptions.Value;
_gameManager = gameManager;
_clientManager = new ClientManager();
_connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_config.ListenIp), _config.ListenPort), IPMode.IPv4, s =>
_serverConfig = serverConfig.Value;
_clientManager = clientManager;
_connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_serverConfig.ListenIp), _serverConfig.ListenPort), IPMode.IPv4, s =>
{
_logger.LogWarning("Log from Hazel: {0}", s);
});
@@ -52,8 +53,8 @@ namespace Impostor.Server.Net
return;
}
// Register client.
_clientManager.Add(new Client(_clientManager, _gameManager, _clientManager.NextId(), clientName, e.Connection));
// Create client.
_clientManager.Create(clientName, e.Connection);
}
public void Start()
+24 -4
View File
@@ -1,27 +1,47 @@
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
{
public class MatchmakerService : IHostedService
internal class MatchmakerService : IHostedService
{
private readonly ILogger<MatchmakerService> _logger;
private readonly ServerConfig _serverConfig;
private readonly ServerRedirectorConfig _redirectorConfig;
private readonly Matchmaker _matchmaker;
public MatchmakerService(ILogger<MatchmakerService> logger, Matchmaker matchmaker)
public MatchmakerService(
ILogger<MatchmakerService> logger,
IOptions<ServerConfig> serverConfig,
IOptions<ServerRedirectorConfig> redirectorConfig,
Matchmaker matchmaker)
{
_logger = logger;
_serverConfig = serverConfig.Value;
_redirectorConfig = redirectorConfig.Value;
_matchmaker = matchmaker;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_matchmaker.Start();
_logger.LogInformation("Matchmaker is running on {0}:{1}.",
_logger.LogInformation("Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.",
_matchmaker.EndPoint.Address,
_matchmaker.EndPoint.Port);
_matchmaker.EndPoint.Port,
_serverConfig.PublicIp,
_serverConfig.PublicPort);
if (_redirectorConfig.Enabled)
{
_logger.LogWarning(_redirectorConfig.Master
? "Server redirection is enabled as master, this instance will redirect clients to other nodes."
: "Server redirection is enabled as node, this instance will accept clients.");
}
return Task.CompletedTask;
}
@@ -0,0 +1,21 @@
using System.Net;
using Hazel;
namespace Impostor.Server.Net.Messages
{
internal static class Message13Redirect
{
public static void Serialize(MessageWriter writer, bool clear, IPEndPoint ipEndPoint)
{
if (clear)
{
writer.Clear(SendOption.Reliable);
}
writer.StartMessage(MessageFlags.Redirect);
writer.Write(ipEndPoint.Address.GetAddressBytes());
writer.Write((ushort) ipEndPoint.Port);
writer.EndMessage();
}
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using Hazel;
using Impostor.Server.Net.Manager;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Redirector
{
internal class ClientManagerRedirector : IClientManager
{
private readonly ILogger<ClientManagerRedirector> _logger;
private readonly INodeProvider _nodeProvider;
private readonly HashSet<ClientRedirector> _clients;
public ClientManagerRedirector(ILogger<ClientManagerRedirector> logger, INodeProvider nodeProvider)
{
_logger = logger;
_nodeProvider = nodeProvider;
_clients = new HashSet<ClientRedirector>();
}
public void Create(string name, Connection connection)
{
_logger.LogInformation("Client connected.");
_clients.Add(new ClientRedirector(name, connection, this, _nodeProvider));
}
public void Remove(ClientRedirector client)
{
_logger.LogInformation("Client disconnected.");
_clients.Remove(client);
}
}
}
@@ -0,0 +1,116 @@
using System;
using Hazel;
using Impostor.Server.Data;
using Impostor.Server.Net.Messages;
using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
using Serilog;
using ILogger = Serilog.ILogger;
namespace Impostor.Server.Net.Redirector
{
internal class ClientRedirector
{
private static readonly ILogger Logger = Log.ForContext<ClientRedirector>();
private readonly string _name;
private readonly Connection _connection;
private readonly ClientManagerRedirector _clientManager;
private readonly INodeProvider _nodeProvider;
public ClientRedirector(string name, Connection connection, ClientManagerRedirector clientManager, INodeProvider nodeProvider)
{
_name = name;
_connection = connection;
_connection.DataReceived += OnDataReceived;
_connection.Disconnected += OnDisconnected;
_clientManager = clientManager;
_nodeProvider = nodeProvider;
}
private void OnDataReceived(DataReceivedEventArgs e)
{
try
{
while (true)
{
if (e.Message.Position >= e.Message.Length)
{
break;
}
OnMessageReceived(e.Message.ReadMessage());
}
}
catch (Exception ex)
{
Logger.Error(ex, "Exception caught in client data handler.");
}
}
private void OnMessageReceived(MessageReader message)
{
var flag = message.Tag;
Logger.Verbose("Server got {0}.", flag);
switch (flag)
{
case MessageFlags.HostGame:
{
using (var packet = MessageWriter.Get(SendOption.Reliable))
{
Message13Redirect.Serialize(packet, false, _nodeProvider.Get());
_connection.Send(packet);
}
break;
}
case MessageFlags.JoinGame:
{
Message01JoinGame.Deserialize(message,
out var gameCode,
out var unknown);
using (var packet = MessageWriter.Get(SendOption.Reliable))
{
var endpoint = _nodeProvider.Find(GameCode.IntToGameName(gameCode));
if (endpoint == null)
{
Message01JoinGame.SerializeError(packet, false, DisconnectReason.GameMissing);
}
else
{
Message13Redirect.Serialize(packet, false, endpoint);
}
_connection.Send(packet);
}
break;
}
case MessageFlags.GetGameListV2:
{
// TODO: Implement.
using (var packet = MessageWriter.Get(SendOption.Reliable))
{
Message01JoinGame.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented);
_connection.Send(packet);
}
break;
}
default:
{
Logger.Warning("Received unsupported message flag on the redirector ({0}).", flag);
break;
}
}
}
private void OnDisconnected(object sender, DisconnectedEventArgs e)
{
_clientManager.Remove(this);
}
}
}
@@ -0,0 +1,12 @@
using System.Net;
namespace Impostor.Server.Net.Redirector
{
internal interface INodeProvider
{
IPEndPoint Get();
IPEndPoint Find(string gameCode);
void Save(string gameCode, IPEndPoint endPoint);
void Remove(string gameCode);
}
}
@@ -0,0 +1,29 @@
using System;
using System.Net;
namespace Impostor.Server.Net.Redirector
{
public class NodeProviderNoOp : INodeProvider
{
public IPEndPoint Get()
{
throw new NotImplementedException();
}
public IPEndPoint Find(string gameCode)
{
// Do nothing.
return null;
}
public void Save(string gameCode, IPEndPoint endPoint)
{
// Do nothing.
}
public void Remove(string gameCode)
{
// Do nothing.
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Net;
using Impostor.Server.Data;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Net.Redirector
{
internal class NodeProviderRedis : INodeProvider
{
private readonly IDistributedCache _cache;
private readonly List<IPEndPoint> _nodes;
private readonly object _lock;
private int _currentIndex;
public NodeProviderRedis(IOptions<ServerRedirectorConfig> redirectorConfig, IDistributedCache cache)
{
_cache = cache;
_nodes = new List<IPEndPoint>();
_lock = new object();
foreach (var node in redirectorConfig.Value.Nodes)
{
_nodes.Add(new IPEndPoint(IPAddress.Parse(node.Ip), node.Port));
}
}
public IPEndPoint Get()
{
lock (_lock)
{
var node = _nodes[_currentIndex++];
if (_currentIndex == _nodes.Count)
{
_currentIndex = 0;
}
return node;
}
}
public IPEndPoint Find(string gameCode)
{
var entry = _cache.GetString(gameCode);
if (entry == null)
{
return null;
}
return IPEndPoint.Parse(entry);
}
public void Save(string gameCode, IPEndPoint endPoint)
{
_cache.SetString(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromHours(1)
});
}
public void Remove(string gameCode)
{
_cache.Remove(gameCode);
}
}
}
@@ -4,7 +4,7 @@ using Impostor.Shared.Innersloth;
namespace Impostor.Server.Net.State
{
public partial class ClientPlayer
internal partial class ClientPlayer
{
/// <summary>
/// Triggered when the connected client requests the game listing.
@@ -5,7 +5,7 @@ using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.State
{
public partial class ClientPlayer
internal partial class ClientPlayer
{
private readonly GameManager _gameManager;
@@ -7,7 +7,7 @@ using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.State
{
public partial class Game
internal partial class Game
{
public void HandleStartGame(MessageReader message)
{
@@ -5,7 +5,7 @@ using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.State
{
public partial class Game
internal partial class Game
{
private void WriteRemovePlayerMessage(MessageWriter message, bool clear, int playerId, DisconnectReason reason)
{
+5 -2
View File
@@ -5,6 +5,7 @@ using System.Net;
using Hazel;
using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Messages;
using Impostor.Server.Net.Redirector;
using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
using Serilog;
@@ -12,17 +13,19 @@ using ILogger = Serilog.ILogger;
namespace Impostor.Server.Net.State
{
public partial class Game
internal partial class Game
{
private static readonly ILogger Logger = Log.ForContext<Game>();
private readonly GameManager _gameManager;
private readonly INodeProvider _nodeProvider;
private readonly ConcurrentDictionary<int, ClientPlayer> _players;
private readonly HashSet<IPAddress> _bannedIps;
public Game(GameManager gameManager, IPEndPoint publicIp, int code, GameOptionsData options)
public Game(GameManager gameManager, INodeProvider nodeProvider, IPEndPoint publicIp, int code, GameOptionsData options)
{
_gameManager = gameManager;
_nodeProvider = nodeProvider;
_players = new ConcurrentDictionary<int, ClientPlayer>();
_bannedIps = new HashSet<IPAddress>();
+33 -5
View File
@@ -2,11 +2,11 @@
using Impostor.Server.Data;
using Impostor.Server.Net;
using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Redirector;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
namespace Impostor.Server
{
@@ -19,9 +19,8 @@ namespace Impostor.Server
.MinimumLevel.Verbose()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
#endif
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
@@ -48,14 +47,43 @@ namespace Impostor.Server
.ConfigureAppConfiguration(builder =>
{
builder.AddJsonFile("config.json", true);
builder.AddJsonFile("config.Development.json", true);
builder.AddEnvironmentVariables(prefix: "IMPOSTOR_");
builder.AddCommandLine(args);
})
.ConfigureServices((host, services) =>
{
services.Configure<ServerConfig>(host.Configuration.GetSection("Server"));
var redirector = host.Configuration
.GetSection(ServerRedirectorConfig.Section)
.Get<ServerRedirectorConfig>();
services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
services.AddSingleton<GameManager>();
if (redirector.Enabled)
{
services.AddSingleton<INodeProvider, NodeProviderRedis>();
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redirector.Redis;
options.InstanceName = "ImpostorRedis";
});
}
else
{
services.AddSingleton<INodeProvider, NodeProviderNoOp>();
}
if (redirector.Enabled && redirector.Master)
{
services.AddSingleton<IClientManager, ClientManagerRedirector>();
}
else
{
services.AddSingleton<IClientManager, ClientManager>();
services.AddSingleton<GameManager>();
}
services.AddSingleton<Matchmaker>();
services.AddHostedService<MatchmakerService>();
+15
View File
@@ -4,5 +4,20 @@
"PublicPort": 22023,
"ListenIp": "0.0.0.0",
"ListenPort": 22023
},
"ServerRedirector": {
"Enabled": false,
"Master": true,
"Redis": "127.0.0.1:6379",
"Nodes": [
{
"Ip": "127.0.0.1",
"Port": 22024
},
{
"Ip": "127.0.0.1",
"Port": 22025
}
]
}
}