Add relay server
This commit is contained in:
parent
c185497502
commit
316fff969e
@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedProject", "SharedProj
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MudAdmin", "MudAdmin\MudAdmin.csproj", "{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCRelayServer", "GCRelayServer\GCRelayServer.csproj", "{268178DF-6345-4D9E-A389-9E809CBF39C9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -27,6 +29,10 @@ Global
|
||||
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
23
GCRelayServer/DictEntry.cs
Normal file
23
GCRelayServer/DictEntry.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Net;
|
||||
|
||||
namespace GCRelayServer;
|
||||
|
||||
public class DictEntry
|
||||
{
|
||||
public List<EndPoint> EndPoints { get; set; } = new();
|
||||
|
||||
public DateTime LastAccessTime { get; set; } = DateTime.Now;
|
||||
|
||||
public void AddEndpoint(EndPoint endPoint, bool shouldClear = false)
|
||||
{
|
||||
if (shouldClear)
|
||||
{
|
||||
EndPoints.Clear();
|
||||
}
|
||||
if (EndPoints.Contains(endPoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
EndPoints.Add(endPoint);
|
||||
}
|
||||
}
|
16
GCRelayServer/GCRelayServer.csproj
Normal file
16
GCRelayServer/GCRelayServer.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BinarySerializer" Version="8.6.3-alpha" />
|
||||
<PackageReference Include="NetCoreServer" Version="6.2.0" />
|
||||
<PackageReference Include="Swan.Logging" Version="6.0.2-beta.69" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
59
GCRelayServer/Program.cs
Normal file
59
GCRelayServer/Program.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System.Net;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace GCRelayServer
|
||||
{
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
#if DEBUG
|
||||
ConsoleLogger.Instance.LogLevel = LogLevel.Debug;
|
||||
#endif
|
||||
// UDP server port
|
||||
var port = 3333;
|
||||
if (args.Length > 0)
|
||||
{
|
||||
port = int.Parse(args[0]);
|
||||
}
|
||||
|
||||
$"UDP server port: {port}".Info();
|
||||
|
||||
// Create a new UDP echo server
|
||||
var server = new RelayServer(IPAddress.Any, port);
|
||||
|
||||
// Start the server
|
||||
"Server starting...".Info();
|
||||
server.Start();
|
||||
"Server started".Info();
|
||||
|
||||
"Press Enter to stop the server or '!' to restart the server...".Info();
|
||||
|
||||
// Perform text input
|
||||
for (;;)
|
||||
{
|
||||
var line = Console.ReadLine();
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Restart the server
|
||||
if (line != "!")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
"Server restarting...".Info();
|
||||
server.Restart();
|
||||
"Server restarted".Info();
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
"Server stopping...".Info();
|
||||
server.Stop();
|
||||
"Server stopped, press any key to close".Info();
|
||||
Console.ReadKey(true);
|
||||
}
|
||||
}
|
||||
}
|
57
GCRelayServer/RelayPacket.cs
Normal file
57
GCRelayServer/RelayPacket.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using BinarySerialization;
|
||||
|
||||
namespace GCRelayServer;
|
||||
|
||||
public class RelayPacket
|
||||
{
|
||||
[FieldOrder(0)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort Magic;
|
||||
|
||||
[FieldOrder(1)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort RemainingSize;
|
||||
|
||||
[FieldOrder(2)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort RequestMainType;
|
||||
|
||||
[FieldOrder(3)]
|
||||
[FieldCount(6)]
|
||||
public byte[] Unknown0 = Array.Empty<byte>();
|
||||
|
||||
[FieldOrder(4)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort RequestSubType;
|
||||
|
||||
[FieldOrder(5)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort Unknown1;
|
||||
|
||||
[FieldOrder(6)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort DataSize;
|
||||
|
||||
[FieldOrder(7)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public ushort Unknown2;
|
||||
|
||||
[FieldOrder(8)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public uint MatchingId;
|
||||
|
||||
[FieldOrder(9)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public uint EntryNo;
|
||||
|
||||
[FieldOrder(10)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public uint MachineId;
|
||||
|
||||
[FieldOrder(11)]
|
||||
[FieldEndianness(Endianness.Big)]
|
||||
public uint Unknown3;
|
||||
|
||||
[FieldOrder(12)]
|
||||
public byte[] Data = Array.Empty<byte>();
|
||||
}
|
14
GCRelayServer/RelayPacketTypes.cs
Normal file
14
GCRelayServer/RelayPacketTypes.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace GCRelayServer;
|
||||
|
||||
public static class RelayPacketTypes
|
||||
{
|
||||
public const ushort HEART_BEAT = 0xB0;
|
||||
|
||||
public const ushort HEART_BEAT_RESPONSE = 0xB1;
|
||||
|
||||
public const ushort START_MATCHING = 0xA0;
|
||||
|
||||
public const ushort START_MATCHING_RESPONSE = 0xA1;
|
||||
|
||||
public const ushort REGISTER_MATCHING = 0xA6;
|
||||
}
|
160
GCRelayServer/RelayServer.cs
Normal file
160
GCRelayServer/RelayServer.cs
Normal file
@ -0,0 +1,160 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using BinarySerialization;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace GCRelayServer;
|
||||
|
||||
public class RelayServer : NetCoreServer.UdpServer
|
||||
{
|
||||
private ConcurrentDictionary<uint, DictEntry> matchingDictionary = new();
|
||||
public RelayServer(IPAddress address, int port) : base(address, port) {}
|
||||
|
||||
|
||||
protected override void OnStarted()
|
||||
{
|
||||
// Start receive datagrams
|
||||
ReceiveAsync();
|
||||
}
|
||||
|
||||
protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
|
||||
{
|
||||
var serializer = new BinarySerializer();
|
||||
var inputStream = new MemoryStream(buffer, (int)offset, (int)size, false);
|
||||
var packet = serializer.Deserialize<RelayPacket>(inputStream);
|
||||
|
||||
if (!IsValidPacket(packet))
|
||||
{
|
||||
"Received malformed packet!".Warn();
|
||||
return;
|
||||
}
|
||||
|
||||
$"Received packet from {endpoint}, type is 0x{packet.RequestSubType:X2}".Info();
|
||||
|
||||
switch (packet.RequestSubType)
|
||||
{
|
||||
case RelayPacketTypes.HEART_BEAT:
|
||||
{
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
SendPacketSingle(endpoint, packet, RelayPacketTypes.HEART_BEAT_RESPONSE);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case RelayPacketTypes.START_MATCHING:
|
||||
{
|
||||
AddEntry(packet.MatchingId, endpoint);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
SendPacketSingle(endpoint, packet, RelayPacketTypes.START_MATCHING_RESPONSE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RelayPacketTypes.REGISTER_MATCHING:
|
||||
{
|
||||
var entry = AddEntry(packet.MatchingId, endpoint);
|
||||
SendPacketToOthers(entry.EndPoints, packet, endpoint);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
var entry = GetEntry(packet.MatchingId);
|
||||
if (entry is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
SendPacketToOthers(entry.EndPoints, packet, endpoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ReceiveAsync();
|
||||
}
|
||||
private void SendPacketSingle(EndPoint endpoint, RelayPacket packet, ushort subType)
|
||||
{
|
||||
packet.RequestSubType = subType;
|
||||
var serializer = new BinarySerializer();
|
||||
var sendStream = new MemoryStream(1024);
|
||||
serializer.Serialize(sendStream, packet);
|
||||
|
||||
$"Send packet to {endpoint}, type is 0x{packet.RequestSubType:X2}".Info();
|
||||
SendAsync(endpoint, sendStream.GetBuffer(), 0, sendStream.Length);
|
||||
}
|
||||
|
||||
private void SendPacketToOthers(IEnumerable<EndPoint> endPoints, RelayPacket packet, EndPoint owner)
|
||||
{
|
||||
if (owner is not IPEndPoint ipEndPoint)
|
||||
{
|
||||
"Endpoint is not IP endpoint! This should not happen!".Fatal();
|
||||
throw new ApplicationException();
|
||||
}
|
||||
|
||||
foreach (var endPoint in endPoints.Where(endPoint => !ipEndPoint.Equals(endPoint)))
|
||||
{
|
||||
SendPacketSingle(endPoint, packet, packet.RequestSubType);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnError(SocketError error)
|
||||
{
|
||||
$"Relay server caught an error with code {error}".Error();
|
||||
}
|
||||
|
||||
private static bool IsValidPacket(RelayPacket packet)
|
||||
{
|
||||
var totalSize = packet.Magic + packet.RemainingSize;
|
||||
var actualSize = 36 + packet.Data.Length;
|
||||
if (totalSize != actualSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return packet.Data.Length == packet.DataSize;
|
||||
}
|
||||
|
||||
private DictEntry AddEntry(uint matchingId, EndPoint endPoint)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var entry = matchingDictionary.GetValueOrDefault(matchingId, new DictEntry());
|
||||
var shouldClear = false;
|
||||
|
||||
if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10))
|
||||
{
|
||||
$"Entry for matching id {matchingId:X8} has expired! Clients will be cleared!".Info();
|
||||
shouldClear = true;
|
||||
}
|
||||
if (entry.EndPoints.Count >= 4)
|
||||
{
|
||||
$"Entry for matching id {matchingId:X8} contains more than 4 clients! Clients will be cleared!".Warn();
|
||||
shouldClear = true;
|
||||
}
|
||||
entry.AddEndpoint(endPoint, shouldClear);
|
||||
|
||||
entry.LastAccessTime = DateTime.Now;
|
||||
matchingDictionary[matchingId] = entry;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private DictEntry? GetEntry(uint matchingId)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
if (!matchingDictionary.ContainsKey(matchingId))
|
||||
{
|
||||
$"Entry for matching id {matchingId:X8} does not exist!".Warn();
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = matchingDictionary[matchingId];
|
||||
if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10))
|
||||
{
|
||||
$"Entry for matching id {matchingId:X8} has expired!".Warn();
|
||||
return null;
|
||||
}
|
||||
|
||||
entry.LastAccessTime = DateTime.Now;
|
||||
return entry;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user