Merge branch 'main' of github.com:Derek-R-S/Light-Reflective-Mirror into docker

This commit is contained in:
Muka Schultze 2021-04-07 19:04:42 -03:00
commit e952c45c28
1100 changed files with 114759 additions and 1807 deletions

BIN
LRM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -12,5 +12,6 @@ namespace LightReflectiveMirror.LoadBalancing
public string AuthKey = "AuthKey"; public string AuthKey = "AuthKey";
public ushort EndpointPort = 7070; public ushort EndpointPort = 7070;
public ushort RelayEndpointPort = 8080; public ushort RelayEndpointPort = 8080;
public bool ShowDebugLogs = false;
} }
} }

View file

@ -0,0 +1,56 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace LightReflectiveMirror.LoadBalancing
{
// for stats
[Serializable]
public struct RelayServerInfo
{
public int connectedClients;
public int roomCount;
public int publicRoomCount;
public TimeSpan uptime;
[JsonIgnore]
public List<Room> serversConnectedToRelay;
}
[Serializable]
internal struct LoadBalancerStats
{
public int nodeCount;
public TimeSpan uptime;
public long CCU;
public long totalServerCount;
}
// container for relay address info
[JsonObject(MemberSerialization.OptOut)]
public struct RelayAddress
{
public ushort port;
public ushort endpointPort;
public string address;
public LRMRegions serverRegion;
[JsonIgnore]
public string endpointAddress;
}
[Serializable]
public struct Room
{
public string serverId;
public int hostId;
public string serverName;
public string serverData;
public bool isPublic;
public int maxPlayers;
public List<int> clients;
public RelayAddress relayInfo;
}
public enum LRMRegions { Any, NorthAmerica, SouthAmerica, Europe, Asia, Africa, Oceania }
}

View file

@ -1,183 +0,0 @@
using Grapevine;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using HttpStatusCode = Grapevine.HttpStatusCode;
namespace LightReflectiveMirror.LoadBalancing
{
[RestResource]
public class Endpoint
{
/// <summary>
/// Sent from an LRM server node
/// adds it to the list if authenticated.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/auth")]
public async Task ReceiveAuthKey(IHttpContext context)
{
var req = context.Request;
string receivedAuthKey = req.Headers["Auth"];
string endpointPort = req.Headers["EndpointPort"];
string gamePort = req.Headers["GamePort"];
string address = context.Request.RemoteEndPoint.Address.ToString();
Console.WriteLine("Received auth req [" + receivedAuthKey + "] == [" + Program.conf.AuthKey + "]");
// if server is authenticated
if (receivedAuthKey != null && address != null && endpointPort != null && gamePort != null && receivedAuthKey == Program.conf.AuthKey)
{
Console.WriteLine($"Server accepted: {address}:{gamePort}");
var _gamePort = Convert.ToUInt16(gamePort);
var _endpointPort = Convert.ToUInt16(endpointPort);
await Program.instance.AddServer(address, _gamePort, _endpointPort);
await context.Response.SendResponseAsync(HttpStatusCode.Ok);
}
else
await context.Response.SendResponseAsync(HttpStatusCode.Forbidden);
}
/// <summary>
/// Hooks into from unity side, client will call this to
/// find the least populated server to join
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/join/")]
public async Task JoinRelay(IHttpContext context)
{
// need to copy over in order to avoid
// collection being modified while iterating.
var servers = Program.instance.availableRelayServers.ToList();
if (servers.Count == 0)
{
await context.Response.SendResponseAsync(HttpStatusCode.ServiceUnavailable);
return;
}
KeyValuePair<RelayAddress, RelayServerInfo> lowest = new(new RelayAddress { Address = "Dummy" }, new RelayServerInfo { ConnectedClients = int.MaxValue });
for (int i = 0; i < servers.Count; i++)
{
if (servers[i].Value.ConnectedClients < lowest.Value.ConnectedClients)
{
lowest = servers[i];
}
}
// respond with the server ip
// if the string is still dummy then theres no servers
if (lowest.Key.Address != "Dummy")
{
// ping server to ensure its online.
var chosenServer = await Program.instance.ManualPingServer(lowest.Key.Address, lowest.Key.EndpointPort);
if (chosenServer.HasValue)
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(lowest.Key));
else
await context.Response.SendResponseAsync(HttpStatusCode.BadGateway);
}
else
{
await context.Response.SendResponseAsync(HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Returns all the servers on all the relay nodes.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/masterlist/")]
public async Task GetMasterServerList(IHttpContext context)
{
var relays = Program.instance.availableRelayServers.ToList();
List<Room> masterList = new();
foreach (var relay in relays)
{
var serversOnRelay = await Program.instance.GetServerListFromIndividualRelay(relay.Key.Address, relay.Key.EndpointPort);
if(serversOnRelay != null)
{
masterList.AddRange(serversOnRelay);
}
else { continue; }
}
// we have servers, send em!
if (masterList.Any())
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(masterList));
// no servers or maybe no relays, fuck you
else
await context.Response.SendResponseAsync(HttpStatusCode.NoContent);
}
}
#region Startup
public class EndpointServer
{
public bool Start(ushort port = 7070)
{
try
{
var config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
var server = new RestServerBuilder(new ServiceCollection(), config,
(services) =>
{
services.AddLogging(configure => configure.AddConsole());
services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.None);
}, (server) =>
{
server.Prefixes.Add($"http://{GetLocalIp()}:{port}/");
server.Prefixes.Add($"http://127.0.0.1:{port}/");
}).Build();
server.Router.Options.SendExceptionMessages = false;
server.Start();
return true;
}
catch
{
return false;
}
}
private static string GetLocalIp()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork && ip.ToString() != "127.0.0.1")
{
return ip.ToString();
}
}
return null;
}
#endregion
}
}

View file

@ -0,0 +1,277 @@
using Grapevine;
using LightReflectiveMirror.Debug;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using HttpStatusCode = Grapevine.HttpStatusCode;
namespace LightReflectiveMirror.LoadBalancing
{
[RestResource]
public partial class Endpoint
{
/// <summary>
/// Sent from an LRM server node
/// adds it to the list if authenticated.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/auth")]
public async Task ReceiveAuthKey(IHttpContext context)
{
var req = context.Request;
string receivedAuthKey = req.Headers["Authorization"];
string endpointPort = req.Headers["x-EndpointPort"];
string gamePort = req.Headers["x-GamePort"];
string publicIP = req.Headers["x-PIP"];
string region = req.Headers["x-Region"];
string address = context.Request.RemoteEndPoint.Address.ToString();
Logger.WriteLogMessage("Received auth req [" + receivedAuthKey + "] == [" + Program.conf.AuthKey + "]");
// if server is authenticated
if (receivedAuthKey != null && region != null && int.TryParse(region, out int regionId) &&
address != null && endpointPort != null && gamePort != null && receivedAuthKey == Program.conf.AuthKey)
{
Logger.WriteLogMessage($"Server accepted: {address}:{gamePort}");
try
{
var _gamePort = Convert.ToUInt16(gamePort);
var _endpointPort = Convert.ToUInt16(endpointPort);
await Program.instance.AddServer(address, _gamePort, _endpointPort, publicIP, regionId);
}
catch
{
await context.Response.SendResponseAsync(HttpStatusCode.BadRequest);
}
await context.Response.SendResponseAsync(HttpStatusCode.Ok);
}
else
await context.Response.SendResponseAsync(HttpStatusCode.Forbidden);
}
/// <summary>
/// Called on the load balancer when a relay node had a change in their servers. This recompiles the cached values.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/roomsupdated")]
public async Task ServerListUpdate(IHttpContext context)
{
// Dont allow unauthorizated access waste computing resources.
string auth = context.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth) && auth == Program.conf.AuthKey)
{
var relays = Program.instance.availableRelayServers.ToList();
ClearAllServersLists();
List<Room> requestedRooms;
for (int i = 0; i < relays.Count; i++)
{
requestedRooms = await Program.instance.RequestServerListFromNode(relays[i].Key.address, relays[i].Key.endpointPort);
_allServers.AddRange(requestedRooms);
switch (relays[i].Key.serverRegion)
{
default:
case (LRMRegions.NorthAmerica):
_northAmericaServers.AddRange(requestedRooms);
break;
case (LRMRegions.SouthAmerica):
_southAmericaServers.AddRange(requestedRooms);
break;
case (LRMRegions.Europe):
_europeServers.AddRange(requestedRooms);
break;
case (LRMRegions.Africa):
_africaServers.AddRange(requestedRooms);
break;
case (LRMRegions.Asia):
_asiaServers.AddRange(requestedRooms);
break;
case (LRMRegions.Oceania):
_oceaniaServers.AddRange(requestedRooms);
break;
}
}
CacheAllServers();
}
}
/// <summary>
/// Hooks into from unity side, client will call this to
/// find the least populated server to join
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/join/")]
public async Task JoinRelay(IHttpContext context)
{
// need to copy over in order to avoid
// collection being modified while iterating.
var servers = Program.instance.availableRelayServers.ToList();
if (servers.Count == 0)
{
await context.Response.SendResponseAsync(HttpStatusCode.RangeNotSatisfiable);
return;
}
KeyValuePair<RelayAddress, RelayServerInfo> lowest = new(new RelayAddress { address = "Dummy" }, new RelayServerInfo { connectedClients = int.MaxValue });
for (int i = 0; i < servers.Count; i++)
{
if (servers[i].Value.connectedClients < lowest.Value.connectedClients)
{
lowest = servers[i];
}
}
// respond with the server ip
// if the string is still dummy then theres no servers
await context.Response.SendResponseAsync(lowest.Key.address != "Dummy" ? JsonConvert.SerializeObject(lowest.Key) : HttpStatusCode.InternalServerError);
}
/// <summary>
/// Returns all the servers on all the relay nodes.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/masterlist/")]
public async Task GetMasterServerList(IHttpContext context)
{
string region = context.Request.Headers["x-Region"];
if(int.TryParse(region, out int regionID))
{
switch ((LRMRegions)regionID)
{
case LRMRegions.Any:
await context.Response.SendResponseAsync(allCachedServers);
break;
case LRMRegions.NorthAmerica:
await context.Response.SendResponseAsync(NorthAmericaCachedServers);
break;
case LRMRegions.SouthAmerica:
await context.Response.SendResponseAsync(SouthAmericaCachedServers);
break;
case LRMRegions.Europe:
await context.Response.SendResponseAsync(EuropeCachedServers);
break;
case LRMRegions.Africa:
await context.Response.SendResponseAsync(AfricaCachedServers);
break;
case LRMRegions.Asia:
await context.Response.SendResponseAsync(AsiaCachedServers);
break;
case LRMRegions.Oceania:
await context.Response.SendResponseAsync(OceaniaCachedServers);
break;
}
return;
}
// They didnt submit a region header, just give them all servers as they probably are viewing in browser.
await context.Response.SendResponseAsync(allCachedServers);
}
/// <summary>
/// Returns stats. you're welcome
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/stats/")]
public async Task GetStats(IHttpContext context)
{
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(_stats));
}
[RestRoute("Get", "/api/get/id")]
public async Task GetServerID(IHttpContext context)
{
await context.Response.SendResponseAsync(Program.instance.GenerateServerID());
}
}
#region Startup
public class EndpointServer
{
public bool Start(ushort port = 7070)
{
try
{
var config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
var server = new RestServerBuilder(new ServiceCollection(), config,
(services) =>
{
services.AddLogging(configure => configure.AddConsole());
services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.None);
}, (server) =>
{
foreach (string ip in GetLocalIps())
{
server.Prefixes.Add($"http://{ip}:{port}/");
}
}).Build();
server.Router.Options.SendExceptionMessages = true;
server.Start();
return true;
}
catch
{
return false;
}
}
private static List<string> GetLocalIps()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
List<string> bindableIPv4Addresses = new();
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
bindableIPv4Addresses.Add(ip.ToString());
}
}
bool hasLocal = false;
for (int i = 0; i < bindableIPv4Addresses.Count; i++)
{
if (bindableIPv4Addresses[i] == "127.0.0.1")
hasLocal = true;
}
if (!hasLocal)
bindableIPv4Addresses.Add("127.0.0.1");
return bindableIPv4Addresses;
}
#endregion
}
}

View file

@ -0,0 +1,29 @@
using Newtonsoft.Json;
namespace LightReflectiveMirror.LoadBalancing
{
public partial class Endpoint
{
static void CacheAllServers()
{
allCachedServers = JsonConvert.SerializeObject(_allServers);
NorthAmericaCachedServers = JsonConvert.SerializeObject(_northAmericaServers);
SouthAmericaCachedServers = JsonConvert.SerializeObject(_southAmericaServers);
EuropeCachedServers = JsonConvert.SerializeObject(_europeServers);
AsiaCachedServers = JsonConvert.SerializeObject(_asiaServers);
AfricaCachedServers = JsonConvert.SerializeObject(_africaServers);
OceaniaCachedServers = JsonConvert.SerializeObject(_oceaniaServers);
}
static void ClearAllServersLists()
{
_northAmericaServers.Clear();
_southAmericaServers.Clear();
_europeServers.Clear();
_asiaServers.Clear();
_africaServers.Clear();
_oceaniaServers.Clear();
_allServers.Clear();
}
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LightReflectiveMirror.LoadBalancing
{
public partial class Endpoint
{
public static string allCachedServers = "[]";
public static string NorthAmericaCachedServers = "[]";
public static string SouthAmericaCachedServers = "[]";
public static string EuropeCachedServers = "[]";
public static string AsiaCachedServers = "[]";
public static string AfricaCachedServers = "[]";
public static string OceaniaCachedServers = "[]";
private static List<Room> _northAmericaServers = new();
private static List<Room> _southAmericaServers = new();
private static List<Room> _europeServers = new();
private static List<Room> _africaServers = new();
private static List<Room> _asiaServers = new();
private static List<Room> _oceaniaServers = new();
private static List<Room> _allServers = new();
private LoadBalancerStats _stats
{
get => new()
{
nodeCount = Program.instance.availableRelayServers.Count,
uptime = DateTime.Now - Program.startupTime,
CCU = Program.instance.GetTotalCCU(),
totalServerCount = Program.instance.GetTotalServers(),
};
}
}
}

View file

@ -0,0 +1,37 @@
using System;
namespace LightReflectiveMirror.Debug
{
public static class Logger
{
private static LogConfiguration _conf;
public static void ConfigureLogger(LogConfiguration config) => _conf = config;
public static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
if(!_conf.sendLogs) { return; }
Console.ForegroundColor = color;
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
}
public static void ForceLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
Console.ForegroundColor = color;
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
}
public struct LogConfiguration
{
public bool sendLogs;
}
}
}

View file

@ -1,218 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace LightReflectiveMirror.LoadBalancing
{
class Program
{
/// <summary>
/// Keeps track of all available relays.
/// Key is server address, value is CCU.
/// </summary>
public Dictionary<RelayAddress, RelayServerInfo> availableRelayServers = new();
private int _pingDelay = 10000;
const string API_PATH = "/api/stats";
readonly string CONFIG_PATH = System.Environment.GetEnvironmentVariable("LRM_LB_CONFIG_PATH") ?? "config.json";
public static Config conf;
public static Program instance;
public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
WriteTitle();
instance = this;
if (!File.Exists(CONFIG_PATH))
{
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(new Config(), Formatting.Indented));
WriteLogMessage("A config.json file was generated. Please configure it to the proper settings and re-run!", ConsoleColor.Yellow);
Console.ReadKey();
Environment.Exit(0);
}
else
{
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
_pingDelay = conf.ConnectedServerPingRate;
if (new EndpointServer().Start(conf.EndpointPort))
WriteLogMessage("Endpoint server started successfully", ConsoleColor.Green);
else
WriteLogMessage("Endpoint server started unsuccessfully", ConsoleColor.Red);
}
var pingThread = new Thread(new ThreadStart(() => PingServers()));
pingThread.Start();
// keep console alive
await Task.Delay(-1);
}
public async Task AddServer(string serverIP, ushort port, ushort endpointPort)
{
var stats = await ManualPingServer(serverIP, endpointPort);
if(stats.HasValue)
availableRelayServers.Add(new RelayAddress { Port = port, EndpointPort = endpointPort, Address = serverIP }, stats.Value);
}
public async Task<RelayServerInfo?> ManualPingServer(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}");
return JsonConvert.DeserializeObject<RelayServerInfo>(receivedStats);
}
catch(Exception e)
{
// Server failed to respond to stats, dont add to load balancer.
return null;
}
}
}
public async Task<List<Room>> GetServerListFromIndividualRelay(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}/api/servers");
return JsonConvert.DeserializeObject<List<Room>>(receivedStats);
}
catch (Exception e)
{
// Server failed to respond
return null;
}
}
}
async Task PingServers()
{
while (true)
{
WriteLogMessage("Pinging " + availableRelayServers.Count + " available relays");
// Create a new list so we can modify the collection in our loop.
var keys = new List<RelayAddress>(availableRelayServers.Keys);
for(int i = 0; i < keys.Count; i++)
{
string url = $"http://{keys[i].Address}:{keys[i].EndpointPort}{API_PATH}";
using (WebClient wc = new WebClient())
{
try
{
var serverStats = wc.DownloadString(url);
var deserializedData = JsonConvert.DeserializeObject<RelayServerInfo>(serverStats);
WriteLogMessage("Server " + keys[i].Address + " still exists, keeping in collection.");
if (availableRelayServers.ContainsKey(keys[i]))
availableRelayServers[keys[i]] = deserializedData;
else
availableRelayServers.Add(keys[i], deserializedData);
}
catch (Exception ex)
{
// server doesnt exist anymore probably
// do more shit here
WriteLogMessage("Server " + keys[i] + " does not exist anymore, removing", ConsoleColor.Red);
availableRelayServers.Remove(keys[i]);
}
}
}
await Task.Delay(_pingDelay);
}
}
void WriteTitle()
{
string t = @"
_ _____ __ __
| | | __ \ | \/ |
| | | |__) | | \ / |
| | | _ / | |\/| |
| |____ | | \ \ | | | | w c(..)o (
|______| |_| \_\ |_| |_| \__(-) __)
_ ____ _____ /\ (
| | / __ \ /\ | __ \ /(_)___)
| | | | | | / \ | | | | w /|
| | | | | | / /\ \ | | | | | \
| |____ | |__| | / ____ \ | |__| | m m copyright monkesoft 2021
|______| \____/ /_/ \_\ |_____/
____ _ _ _ _____ ______ _____
| _ \ /\ | | /\ | \ | | / ____| | ____| | __ \
| |_) | / \ | | / \ | \| | | | | |__ | |__) |
| _ < / /\ \ | | / /\ \ | . ` | | | | __| | _ /
| |_) | / ____ \ | |____ / ____ \ | |\ | | |____ | |____ | | \ \
|____/ /_/ \_\ |______| /_/ \_\ |_| \_| \_____| |______| |_| \_\
";
string load = $"Chimp Event Listener Initializing... OK" +
"\nHarambe Memorial Initializing... OK" +
"\nBananas Initializing... OK\n";
WriteLogMessage(t, ConsoleColor.Green);
WriteLogMessage(load, ConsoleColor.Cyan);
}
static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
Console.ForegroundColor = color;
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
}
}
// for stats
[Serializable]
public struct RelayServerInfo
{
public int ConnectedClients;
public int RoomCount;
public int PublicRoomCount;
public TimeSpan Uptime;
}
// container for relay address info
[Serializable]
public struct RelayAddress
{
public ushort Port;
public ushort EndpointPort;
public string Address;
}
[Serializable]
public struct Room
{
public int serverId;
public int hostId;
public string serverName;
public string serverData;
public bool isPublic;
public int maxPlayers;
public List<int> clients;
public RelayAddress relayInfo;
}
}

View file

@ -0,0 +1,233 @@
using LightReflectiveMirror.Debug;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace LightReflectiveMirror.LoadBalancing
{
partial class Program
{
/// <summary>
/// Keeps track of all the LRM nodes registered to the Load Balancer.
/// </summary>
public Dictionary<RelayAddress, RelayServerInfo> availableRelayServers = new();
private int _pingDelay = 10000;
public static bool showDebugLogs = false;
public static DateTime startupTime;
const string API_PATH = "/api/stats";
readonly string CONFIG_PATH = System.Environment.GetEnvironmentVariable("LRM_LB_CONFIG_PATH") ?? "config.json";
public static Config conf;
public static Program instance;
public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
WriteTitle();
instance = this;
startupTime = DateTime.Now;
if (!File.Exists(CONFIG_PATH))
{
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(new Config(), Formatting.Indented));
Logger.ForceLogMessage("A config.json file was generated. Please configure it to the proper settings and re-run!", ConsoleColor.Yellow);
Console.ReadKey();
Environment.Exit(0);
}
else
{
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
Logger.ConfigureLogger(new Logger.LogConfiguration { sendLogs = conf.ShowDebugLogs });
_pingDelay = conf.ConnectedServerPingRate;
showDebugLogs = conf.ShowDebugLogs;
if (new EndpointServer().Start(conf.EndpointPort))
Logger.ForceLogMessage("Endpoint server started successfully", ConsoleColor.Green);
else
Logger.ForceLogMessage("Endpoint server started unsuccessfully", ConsoleColor.Red);
}
var pingThread = new Thread(new ThreadStart(PingServers));
pingThread.Start();
// keep console alive
await Task.Delay(-1);
}
/// <summary>
/// Called when a new server requested that we add them to our load balancer.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <param name="endpointPort"></param>
/// <param name="publicIP"></param>
/// <returns></returns>
public async Task AddServer(string serverIP, ushort port, ushort endpointPort, string publicIP, int regionId)
{
var relayAddr = new RelayAddress { port = port, endpointPort = endpointPort, address = publicIP, endpointAddress = serverIP.Trim(), serverRegion = (LRMRegions)regionId };
if (availableRelayServers.ContainsKey(relayAddr))
{
Logger.ForceLogMessage($"LRM Node {serverIP}:{port} tried to register while already registered!");
return;
}
var stats = await RequestStatsFromNode(serverIP, endpointPort);
if (stats.HasValue)
{
Logger.ForceLogMessage($"LRM Node Registered! {serverIP}:{port}", ConsoleColor.Green);
availableRelayServers.Add(relayAddr, stats.Value);
}
}
/// <summary>
/// Called when we want to get the server info from a server.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<RelayServerInfo?> RequestStatsFromNode(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}");
var stats = JsonConvert.DeserializeObject<RelayServerInfo>(receivedStats);
if (stats.serversConnectedToRelay == null)
stats.serversConnectedToRelay = new List<Room>();
return stats;
}
catch (Exception e)
{
// Server failed to respond to stats, dont add to load balancer.
return null;
}
}
}
/// <summary>
/// Called when we want to check if a server is alive.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<bool> HealthCheckNode(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}");
// If it got to here, then the server is healthy!
return true;
}
catch (Exception e)
{
// Server failed to respond
return false;
}
}
}
/// <summary>
/// Called when we want to get the list of rooms in a specific LRM node.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<List<Room>> RequestServerListFromNode(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}/api/servers");
var stats = JsonConvert.DeserializeObject<List<Room>>(receivedStats);
// If they have no servers, it will return null as json for some reason.
if (stats == null)
return new List<Room>();
else
return stats;
}
catch (Exception e)
{
// Server failed to respond
return new List<Room>();
}
}
}
/// <summary>
/// A thread constantly running and making sure LRM nodes are still healthy.
/// </summary>
async void PingServers()
{
while (true)
{
Logger.WriteLogMessage("Pinging " + availableRelayServers.Count + " available relays");
// Create a new list so we can modify the collection in our loop.
var keys = new List<RelayAddress>(availableRelayServers.Keys);
for (int i = 0; i < keys.Count; i++)
{
if(!await HealthCheckNode(keys[i].endpointAddress, keys[i].endpointPort))
{
Logger.ForceLogMessage($"Server {keys[i].address}:{keys[i].port} failed a health check, removing from load balancer.", ConsoleColor.Red);
availableRelayServers.Remove(keys[i]);
}
}
GC.Collect();
await Task.Delay(_pingDelay);
}
}
void WriteTitle()
{
string t = @"
_ _____ __ __
| | | __ \ | \/ |
| | | |__) | | \ / |
| | | _ / | |\/| |
| |____ | | \ \ | | | | w c(..)o (
|______| |_| \_\ |_| |_| \__(-) __)
_ ____ _____ /\ (
| | / __ \ /\ | __ \ /(_)___)
| | | | | | / \ | | | | w /|
| | | | | | / /\ \ | | | | | \
| |____ | |__| | / ____ \ | |__| | m m copyright monkesoft 2021
|______| \____/ /_/ \_\ |_____/
____ _ _ _ _____ ______ _____
| _ \ /\ | | /\ | \ | | / ____| | ____| | __ \
| |_) | / \ | | / \ | \| | | | | |__ | |__) |
| _ < / /\ \ | | / /\ \ | . ` | | | | __| | _ /
| |_) | / ____ \ | |____ / ____ \ | |\ | | |____ | |____ | | \ \
|____/ /_/ \_\ |______| /_/ \_\ |_| \_| \_____| |______| |_| \_\
";
string load = $"Chimp Event Listener Initializing... OK" +
"\nHarambe Memorial Initializing... OK" +
"\nBananas Initializing... OK\n";
Logger.ForceLogMessage(t, ConsoleColor.Green);
Logger.ForceLogMessage(load, ConsoleColor.Cyan);
}
}
}

View file

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Linq;
namespace LightReflectiveMirror.LoadBalancing
{
partial class Program
{
public long GetTotalCCU()
{
long temp = 0;
foreach (var item in availableRelayServers)
temp += item.Value.connectedClients;
return temp;
}
public long GetTotalServers()
{
int temp = 0;
foreach (var item in availableRelayServers)
temp += item.Value.roomCount;
return temp;
}
public string GenerateServerID()
{
const int LENGTH = 5;
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var randomID = "";
do
{
var random = new System.Random();
randomID = new string(Enumerable.Repeat(chars, LENGTH)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
while (DoesServerIdExist(randomID));
return randomID;
}
/// <summary>
/// Checks if a server id already is in use.
/// </summary>
/// <param name="id">The ID to check for</param>
/// <returns></returns>
bool DoesServerIdExist(string id)
{
var infos = availableRelayServers.Values;
foreach (var info in infos)
{
foreach (var server in info.serversConnectedToRelay)
{
if (server.serverId == id)
return true;
}
}
return false;
}
}
}

View file

@ -1,5 +1,26 @@
![Logo](LRM.png)
# Light Reflective Mirror # Light Reflective Mirror
[![Maintainability](https://api.codeclimate.com/v1/badges/954d1b30c2da8f61037e/maintainability)](https://codeclimate.com/github/Derek-R-S/Light-Reflective-Mirror/maintainability)
LRM Node / MultiCompiled
[![Build status](http://monk3.xyz:90/api/projects/status/p5g03jifksxvkjct/branch/main?retina=true)](http://monk3.xyz:90/project/AppVeyor/light-reflective-mirror/branch/main)
LoadBalancer
[![Build status](http://monk3.xyz:90/api/projects/status/kh6awelf16hl5um4/branch/main?retina=true)](http://monk3.xyz:90/project/AppVeyor/light-reflective-mirror-canqw/branch/main)
Unity Package
[![Build status](http://monk3.xyz:90/api/projects/status/n7kiywl2ls67pn5c?retina=true)](http://monk3.xyz:90/project/AppVeyor/light-reflective-mirror-wdkoo)
## What ## What
Light Reflective Mirror is a transport for Mirror Networking which relays network traffic through your own servers. This allows you to have clients host game servers and not worry about NAT/Port Forwarding, etc. There are still features I plan on adding but it still is completely stable in its current state. Light Reflective Mirror is a transport for Mirror Networking which relays network traffic through your own servers. This allows you to have clients host game servers and not worry about NAT/Port Forwarding, etc. There are still features I plan on adding but it still is completely stable in its current state.
@ -8,13 +29,9 @@ Light Reflective Mirror is a transport for Mirror Networking which relays networ
* Built in server list! * Built in server list!
* Relay password to stop other games from stealing your precious relay! * Relay password to stop other games from stealing your precious relay!
* Relay supports connecting users without them needing to port forward! * Relay supports connecting users without them needing to port forward!
* NAT Punchtrough * NAT Punchtrough (Full Cone, Restricted Cone, and Port Restricted Cone)
* Direct Connecting * Direct Connecting
* Load Balancing with multi-relay setup
## Plans
For the future I plan on adding features such as:
* Multi Relay server setup for load balancing (It will split players up between multiple relay servers to make sure one single relay server isnt doing all the heavy lifting)
## How does it work? ## How does it work?
@ -67,11 +84,10 @@ TransportDLL - This is the name of the dll of the compiled transport dll.
TransportClass - The class name of the transport inside the DLL, Including namespaces! TransportClass - The class name of the transport inside the DLL, Including namespaces!
By default, there are 5 compiled transports in the MultiCompiled dll. By default, there are 5 compiled transports in the MultiCompiled dll.
To switch between them you have the following options: To switch between them you have the following options:
* Mirror.LiteNetLibTransport
* Mirror.TelepathyTransport * Mirror.TelepathyTransport
* kcp2k.KcpTransport * kcp2k.KcpTransport
* Mirror.SimpleWebTransport * Mirror.SimpleWebTransport
* Mirror.MultiplexTransport
AuthenticationKey - This is the key the clients need to have on their inspector. It cannot be blank. AuthenticationKey - This is the key the clients need to have on their inspector. It cannot be blank.

View file

@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.31129.286 VisualStudioVersion = 16.0.31129.286
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LRM", "LRM\LRM.csproj", "{BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LRM", "LRM\LRM.csproj", "{BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiCompiled", "MultiCompiled\MultiCompiled.csproj", "{E4D2AED5-E46A-49BE-B7F1-6BF8A5ADE572}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -15,6 +17,10 @@ Global
{BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Release|Any CPU.Build.0 = Release|Any CPU {BA0E55C8-6B24-4690-AC55-1DDDB4F7C05F}.Release|Any CPU.Build.0 = Release|Any CPU
{E4D2AED5-E46A-49BE-B7F1-6BF8A5ADE572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4D2AED5-E46A-49BE-B7F1-6BF8A5ADE572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4D2AED5-E46A-49BE-B7F1-6BF8A5ADE572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4D2AED5-E46A-49BE-B7F1-6BF8A5ADE572}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -12,6 +12,7 @@ namespace LightReflectiveMirror
public string TransportDLL = "MultiCompiled.dll"; public string TransportDLL = "MultiCompiled.dll";
public string TransportClass = "Mirror.SimpleWebTransport"; public string TransportClass = "Mirror.SimpleWebTransport";
public string AuthenticationKey = "Secret Auth Key"; public string AuthenticationKey = "Secret Auth Key";
public ushort TransportPort = 7777;
public int UpdateLoopTime = 10; public int UpdateLoopTime = 10;
public int UpdateHeartbeatInterval = 100; public int UpdateHeartbeatInterval = 100;
@ -35,5 +36,6 @@ namespace LightReflectiveMirror
public string LoadBalancerAuthKey = "AuthKey"; public string LoadBalancerAuthKey = "AuthKey";
public string LoadBalancerAddress = "127.0.0.1"; public string LoadBalancerAddress = "127.0.0.1";
public ushort LoadBalancerPort = 7070; public ushort LoadBalancerPort = 7070;
public LRMRegions LoadBalancerRegion = LRMRegions.NorthAmerica;
} }
} }

View file

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LightReflectiveMirror.Endpoints namespace LightReflectiveMirror.Endpoints
@ -22,7 +23,11 @@ namespace LightReflectiveMirror.Endpoints
[RestResource] [RestResource]
public class Endpoint public class Endpoint
{ {
private List<Room> _rooms { get => Program.instance.GetRooms(); } private static string _cachedServerList = "[]";
private static string _cachedCompressedServerList;
public static DateTime lastPing = DateTime.Now;
private static List<Room> _rooms { get => Program.instance.GetRooms().Where(x => x.isPublic).ToList(); }
private RelayStats _stats { get => new RelayStats private RelayStats _stats { get => new RelayStats
{ {
@ -32,9 +37,19 @@ namespace LightReflectiveMirror.Endpoints
Uptime = Program.instance.GetUptime() Uptime = Program.instance.GetUptime()
}; } }; }
public static void RoomsModified()
{
_cachedServerList = JsonConvert.SerializeObject(_rooms, Formatting.Indented);
_cachedCompressedServerList = _cachedServerList.Compress();
if (Program.conf.UseLoadBalancer)
Program.instance.UpdateLoadbalancerServers();
}
[RestRoute("Get", "/api/stats")] [RestRoute("Get", "/api/stats")]
public async Task Stats(IHttpContext context) public async Task Stats(IHttpContext context)
{ {
lastPing = DateTime.Now;
string json = JsonConvert.SerializeObject(_stats, Formatting.Indented); string json = JsonConvert.SerializeObject(_stats, Formatting.Indented);
await context.Response.SendResponseAsync(json); await context.Response.SendResponseAsync(json);
} }
@ -44,8 +59,7 @@ namespace LightReflectiveMirror.Endpoints
{ {
if (Program.conf.EndpointServerList) if (Program.conf.EndpointServerList)
{ {
string json = JsonConvert.SerializeObject(_rooms, Formatting.Indented); await context.Response.SendResponseAsync(_cachedServerList);
await context.Response.SendResponseAsync(json);
} }
else else
await context.Response.SendResponseAsync(HttpStatusCode.Forbidden); await context.Response.SendResponseAsync(HttpStatusCode.Forbidden);
@ -56,8 +70,7 @@ namespace LightReflectiveMirror.Endpoints
{ {
if (Program.conf.EndpointServerList) if (Program.conf.EndpointServerList)
{ {
string json = JsonConvert.SerializeObject(_rooms); await context.Response.SendResponseAsync(_cachedCompressedServerList);
await context.Response.SendResponseAsync(json.Compress());
} }
else else
await context.Response.SendResponseAsync(HttpStatusCode.Forbidden); await context.Response.SendResponseAsync(HttpStatusCode.Forbidden);

View file

@ -22,4 +22,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
</Project> </Project>

View file

@ -13,37 +13,9 @@ using Newtonsoft.Json;
namespace LightReflectiveMirror namespace LightReflectiveMirror
{ {
class Program partial class Program
{ {
public static Transport transport;
public static Program instance;
public static Config conf;
private RelayHandler _relay;
private MethodInfo _awakeMethod;
private MethodInfo _startMethod;
private MethodInfo _updateMethod;
private MethodInfo _lateUpdateMethod;
private DateTime _startupTime;
public static string publicIP;
private List<int> _currentConnections = new List<int>();
public Dictionary<int, IPEndPoint> NATConnections = new Dictionary<int, IPEndPoint>();
private BiDictionary<int, string> _pendingNATPunches = new BiDictionary<int, string>();
private int _currentHeartbeatTimer = 0;
private byte[] _NATRequest = new byte[500];
private int _NATRequestPosition = 0;
private UdpClient _punchServer;
private readonly string CONFIG_PATH = System.Environment.GetEnvironmentVariable("LRM_CONFIG_PATH") ?? "config.json";
public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public int GetConnections() => _currentConnections.Count;
public TimeSpan GetUptime() => DateTime.Now - _startupTime;
public int GetPublicRoomCount() => _relay.rooms.Where(x => x.isPublic).Count();
public List<Room> GetRooms() => _relay.rooms; public List<Room> GetRooms() => _relay.rooms;
public async Task MainAsync() public async Task MainAsync()
@ -51,7 +23,8 @@ namespace LightReflectiveMirror
WriteTitle(); WriteTitle();
instance = this; instance = this;
_startupTime = DateTime.Now; _startupTime = DateTime.Now;
publicIP = new WebClient().DownloadString("http://icanhazip.com").Replace("\\r\\n", "").Replace("\\n", "").Trim(); using (WebClient wc = new WebClient())
publicIP = wc.DownloadString("http://ipv4.icanhazip.com").Replace("\\r", "").Replace("\\n", "").Trim();
if (!File.Exists(CONFIG_PATH)) if (!File.Exists(CONFIG_PATH))
{ {
@ -64,6 +37,15 @@ namespace LightReflectiveMirror
{ {
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH)); conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
// Docker variables.
try
{
conf.EndpointPort = ushort.Parse(Environment.GetEnvironmentVariable("LRM_ENDPOINT_PORT"));
conf.TransportPort = ushort.Parse(Environment.GetEnvironmentVariable("LRM_TRANSPORT_PORT"));
}
catch { }
WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true); WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true);
try try
{ {
@ -131,18 +113,19 @@ namespace LightReflectiveMirror
_pendingNATPunches.Remove(clientID); _pendingNATPunches.Remove(clientID);
}; };
transport.ServerStart(); transport.ServerStart(conf.TransportPort);
WriteLogMessage("OK", ConsoleColor.Green); WriteLogMessage("OK", ConsoleColor.Green);
if (conf.UseEndpoint) if (conf.UseEndpoint)
{ {
WriteLogMessage("\nStarting Endpoint Service... ", ConsoleColor.White, true); WriteLogMessage("\nStarting Endpoint Service... ", ConsoleColor.White, true);
var endpoint = new EndpointServer(); var endpointService = new EndpointServer();
if (endpoint.Start(conf.EndpointPort)) if (endpointService.Start(conf.EndpointPort))
{ {
WriteLogMessage("OK", ConsoleColor.Green); WriteLogMessage("OK", ConsoleColor.Green);
Endpoint.RoomsModified();
} }
else else
{ {
@ -193,7 +176,7 @@ namespace LightReflectiveMirror
Environment.Exit(0); Environment.Exit(0);
} }
if(conf.UseLoadBalancer) if (conf.UseLoadBalancer)
await RegisterSelfToLoadBalancer(); await RegisterSelfToLoadBalancer();
} }
@ -211,6 +194,15 @@ namespace LightReflectiveMirror
for(int i = 0; i < _currentConnections.Count; i++) for(int i = 0; i < _currentConnections.Count; i++)
transport.ServerSend(_currentConnections[i], 0, new ArraySegment<byte>(new byte[] { 200 })); transport.ServerSend(_currentConnections[i], 0, new ArraySegment<byte>(new byte[] { 200 }));
if (conf.UseLoadBalancer)
{
if (DateTime.Now > Endpoint.lastPing.AddSeconds(60))
{
// Dont await that on main thread. It would cause a lag spike for clients.
RegisterSelfToLoadBalancer();
}
}
GC.Collect(); GC.Collect();
} }
@ -218,19 +210,38 @@ namespace LightReflectiveMirror
} }
} }
private async Task<bool> RegisterSelfToLoadBalancer() public async void UpdateLoadbalancerServers()
{ {
try
{
using (WebClient wc = new())
{
wc.Headers.Add("Authorization", conf.LoadBalancerAuthKey);
await wc.DownloadStringTaskAsync($"http://{conf.LoadBalancerAddress}:{conf.LoadBalancerPort}/api/roomsupdated");
}
}
catch {} // LLB might be down, ignore.
}
private async Task<bool> RegisterSelfToLoadBalancer()
{
Endpoint.lastPing = DateTime.Now;
try try
{ {
// replace hard coded value for config value later // replace hard coded value for config value later
if (conf.LoadBalancerAddress.ToLower() == "localhost")
conf.LoadBalancerAddress = "127.0.0.1";
var uri = new Uri($"http://{conf.LoadBalancerAddress}:{conf.LoadBalancerPort}/api/auth"); var uri = new Uri($"http://{conf.LoadBalancerAddress}:{conf.LoadBalancerPort}/api/auth");
string endpointPort = conf.EndpointPort.ToString(); string endpointPort = conf.EndpointPort.ToString();
string gamePort = 7777.ToString(); string gamePort = conf.TransportPort.ToString();
HttpWebRequest authReq = (HttpWebRequest)WebRequest.Create(uri); HttpWebRequest authReq = (HttpWebRequest)WebRequest.Create(uri);
authReq.Headers.Add("Auth", conf.LoadBalancerAuthKey); authReq.Headers.Add("Authorization", conf.LoadBalancerAuthKey);
authReq.Headers.Add("EndpointPort", endpointPort); authReq.Headers.Add("x-EndpointPort", endpointPort);
authReq.Headers.Add("GamePort", gamePort); authReq.Headers.Add("x-GamePort", gamePort);
authReq.Headers.Add("x-PIP", publicIP); // Public IP
authReq.Headers.Add("x-Region", ((int)conf.LoadBalancerRegion).ToString());
var res = await authReq.GetResponseAsync(); var res = await authReq.GetResponseAsync();
@ -242,58 +253,6 @@ namespace LightReflectiveMirror
WriteLogMessage("Error registering - Load Balancer probably timed out.", ConsoleColor.Red); WriteLogMessage("Error registering - Load Balancer probably timed out.", ConsoleColor.Red);
return false; return false;
} }
}
void RunNATPunchLoop()
{
WriteLogMessage("OK\n", ConsoleColor.Green);
IPEndPoint remoteEndpoint = new(IPAddress.Any, conf.NATPunchtroughPort);
// Stock Data server sends to everyone:
var serverResponse = new byte[1] { 1 };
byte[] readData;
bool isConnectionEstablishment;
int pos;
string connectionID;
while (true)
{
readData = _punchServer.Receive(ref remoteEndpoint);
pos = 0;
try
{
isConnectionEstablishment = readData.ReadBool(ref pos);
if (isConnectionEstablishment)
{
connectionID = readData.ReadString(ref pos);
if (_pendingNATPunches.TryGetBySecond(connectionID, out pos))
{
NATConnections.Add(pos, new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port));
_pendingNATPunches.Remove(pos);
Console.WriteLine("Client Successfully Established Puncher Connection. " + remoteEndpoint.ToString());
}
}
_punchServer.Send(serverResponse, 1, remoteEndpoint);
}
catch
{
// ignore, packet got fucked up or something.
}
}
}
static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
Console.ForegroundColor = color;
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
} }
void CheckMethods(Type type) void CheckMethods(Type type)
@ -303,26 +262,5 @@ namespace LightReflectiveMirror
_updateMethod = type.GetMethod("Update", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _updateMethod = type.GetMethod("Update", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
_lateUpdateMethod = type.GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _lateUpdateMethod = type.GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
} }
void WriteTitle()
{
string t = @"
w c(..)o (
_ _____ __ __ \__(-) __)
| | | __ \ | \/ | /\ (
| | | |__) || \ / | /(_)___)
| | | _ / | |\/| | w /|
| |____ | | \ \ | | | | | \
|______||_| \_\|_| |_| m m copyright monkesoft 2021
";
string load = $"Chimp Event Listener Initializing... OK" +
"\nHarambe Memorial Initializing... OK" +
"\nBananas Initializing... OK\n";
WriteLogMessage(t, ConsoleColor.Green);
WriteLogMessage(load, ConsoleColor.Cyan);
}
} }
} }

View file

@ -0,0 +1,43 @@
using System;
using System.Linq;
namespace LightReflectiveMirror
{
partial class Program
{
public int GetConnections() => _currentConnections.Count;
public TimeSpan GetUptime() => DateTime.Now - _startupTime;
public int GetPublicRoomCount() => _relay.rooms.Where(x => x.isPublic).Count();
static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
Console.ForegroundColor = color;
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
}
void WriteTitle()
{
string t = @"
w c(..)o (
_ _____ __ __ \__(-) __)
| | | __ \ | \/ | /\ (
| | | |__) || \ / | /(_)___)
| | | _ / | |\/| | w /|
| |____ | | \ \ | | | | | \
|______||_| \_\|_| |_| m m copyright monkesoft 2021
";
string load = $"Chimp Event Listener Initializing... OK" +
"\nHarambe Memorial Initializing... OK" +
"\nBananas Initializing... OK\n";
WriteLogMessage(t, ConsoleColor.Green);
WriteLogMessage(load, ConsoleColor.Cyan);
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Net;
namespace LightReflectiveMirror
{
partial class Program
{
void RunNATPunchLoop()
{
WriteLogMessage("OK\n", ConsoleColor.Green);
IPEndPoint remoteEndpoint = new(IPAddress.Any, conf.NATPunchtroughPort);
// Stock Data server sends to everyone:
var serverResponse = new byte[1] { 1 };
byte[] readData;
bool isConnectionEstablishment;
int pos;
string connectionID;
while (true)
{
readData = _punchServer.Receive(ref remoteEndpoint);
pos = 0;
try
{
isConnectionEstablishment = readData.ReadBool(ref pos);
if (isConnectionEstablishment)
{
connectionID = readData.ReadString(ref pos);
if (_pendingNATPunches.TryGetBySecond(connectionID, out pos))
{
NATConnections.Add(pos, new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port));
_pendingNATPunches.Remove(pos);
Console.WriteLine("Client Successfully Established Puncher Connection. " + remoteEndpoint.ToString());
}
}
_punchServer.Send(serverResponse, 1, remoteEndpoint);
}
catch
{
// ignore, packet got fucked up or something.
}
}
}
}
}

View file

@ -0,0 +1,39 @@
using LightReflectiveMirror.Endpoints;
using Mirror;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
namespace LightReflectiveMirror
{
partial class Program
{
public static Transport transport;
public static Program instance;
public static Config conf;
private RelayHandler _relay;
private MethodInfo _awakeMethod;
private MethodInfo _startMethod;
private MethodInfo _updateMethod;
private MethodInfo _lateUpdateMethod;
private DateTime _startupTime;
public static string publicIP;
private List<int> _currentConnections = new List<int>();
public Dictionary<int, IPEndPoint> NATConnections = new Dictionary<int, IPEndPoint>();
private BiDictionary<int, string> _pendingNATPunches = new BiDictionary<int, string>();
private int _currentHeartbeatTimer = 0;
private byte[] _NATRequest = new byte[500];
private int _NATRequestPosition = 0;
private UdpClient _punchServer;
private readonly string CONFIG_PATH = System.Environment.GetEnvironmentVariable("LRM_CONFIG_PATH") ?? "config.json";
}
public enum LRMRegions { Any, NorthAmerica, SouthAmerica, Europe, Asia, Africa, Oceania }
}

View file

@ -1,349 +0,0 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net;
using System.Text;
namespace LightReflectiveMirror
{
public class RelayHandler
{
public List<Room> rooms = new List<Room>();
private List<int> _pendingAuthentication = new List<int>();
private ArrayPool<byte> _sendBuffers;
private int _maxPacketSize = 0;
public RelayHandler(int maxPacketSize)
{
this._maxPacketSize = maxPacketSize;
_sendBuffers = ArrayPool<byte>.Create(maxPacketSize, 50);
}
public void ClientConnected(int clientId)
{
_pendingAuthentication.Add(clientId);
var buffer = _sendBuffers.Rent(1);
int pos = 0;
buffer.WriteByte(ref pos, (byte)OpCodes.AuthenticationRequest);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(buffer, 0, pos));
_sendBuffers.Return(buffer);
}
public void HandleMessage(int clientId, ArraySegment<byte> segmentData, int channel)
{
try
{
var data = segmentData.Array;
int pos = segmentData.Offset;
OpCodes opcode = (OpCodes)data.ReadByte(ref pos);
if (_pendingAuthentication.Contains(clientId))
{
if (opcode == OpCodes.AuthenticationResponse)
{
string authResponse = data.ReadString(ref pos);
if (authResponse == Program.conf.AuthenticationKey)
{
_pendingAuthentication.Remove(clientId);
int writePos = 0;
var sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref writePos, (byte)OpCodes.Authenticated);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, writePos));
}
}
return;
}
switch (opcode)
{
case OpCodes.CreateRoom:
CreateRoom(clientId, data.ReadInt(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadInt(ref pos));
break;
case OpCodes.RequestID:
SendClientID(clientId);
break;
case OpCodes.LeaveRoom:
LeaveRoom(clientId);
break;
case OpCodes.JoinServer:
JoinRoom(clientId, data.ReadInt(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos));
break;
case OpCodes.KickPlayer:
LeaveRoom(data.ReadInt(ref pos), clientId);
break;
case OpCodes.SendData:
ProcessData(clientId, data.ReadBytes(ref pos), channel, data.ReadInt(ref pos));
break;
case OpCodes.UpdateRoomData:
var plyRoom = GetRoomForPlayer(clientId);
if (plyRoom == null)
return;
bool newName = data.ReadBool(ref pos);
if (newName)
plyRoom.serverName = data.ReadString(ref pos);
bool newData = data.ReadBool(ref pos);
if (newData)
plyRoom.serverData = data.ReadString(ref pos);
bool newPublicStatus = data.ReadBool(ref pos);
if (newPublicStatus)
plyRoom.isPublic = data.ReadBool(ref pos);
bool newPlayerCap = data.ReadBool(ref pos);
if (newPlayerCap)
plyRoom.maxPlayers = data.ReadInt(ref pos);
break;
}
}
catch
{
// Do Nothing. Client probably sent some invalid data.
}
}
public void HandleDisconnect(int clientId) => LeaveRoom(clientId);
void ProcessData(int clientId, byte[] clientData, int channel, int sendTo = -1)
{
Room playersRoom = GetRoomForPlayer(clientId);
if(playersRoom != null)
{
Room room = playersRoom;
if(room.hostId == clientId)
{
if (room.clients.Contains(sendTo))
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(_maxPacketSize);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData);
sendBuffer.WriteBytes(ref pos, clientData);
Program.transport.ServerSend(sendTo, channel, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
}
else
{
// We are not the host, so send the data to the host.
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(_maxPacketSize);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData);
sendBuffer.WriteBytes(ref pos, clientData);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(room.hostId, channel, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
}
}
Room GetRoomForPlayer(int clientId)
{
for(int i = 0; i < rooms.Count; i++)
{
if (rooms[i].hostId == clientId)
return rooms[i];
if (rooms[i].clients.Contains(clientId))
return rooms[i];
}
return null;
}
void JoinRoom(int clientId, int serverId, bool canDirectConnect, string localIP)
{
LeaveRoom(clientId);
for(int i = 0; i < rooms.Count; i++)
{
if(rooms[i].serverId == serverId)
{
if(rooms[i].clients.Count < rooms[i].maxPlayers)
{
rooms[i].clients.Add(clientId);
int sendJoinPos = 0;
byte[] sendJoinBuffer = _sendBuffers.Rent(500);
if (canDirectConnect && Program.instance.NATConnections.ContainsKey(clientId))
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
if (Program.instance.NATConnections[clientId].Address.Equals(rooms[i].hostIP.Address))
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostLocalIP == localIP ? "127.0.0.1" : rooms[i].hostLocalIP);
else
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostIP.Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, rooms[i].useNATPunch ? rooms[i].hostIP.Port : rooms[i].port);
sendJoinBuffer.WriteBool(ref sendJoinPos, rooms[i].useNATPunch);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
if (rooms[i].useNATPunch)
{
sendJoinPos = 0;
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
Console.WriteLine(Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteString(ref sendJoinPos, Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, Program.instance.NATConnections[clientId].Port);
sendJoinBuffer.WriteBool(ref sendJoinPos, true);
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
}
_sendBuffers.Return(sendJoinBuffer);
return;
}
else
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.ServerJoined);
sendJoinBuffer.WriteInt(ref sendJoinPos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
_sendBuffers.Return(sendJoinBuffer);
return;
}
}
}
}
// If it got to here, then the server was not found, or full. Tell the client.
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
void CreateRoom(int clientId, int maxPlayers, string serverName, bool isPublic, string serverData, bool useDirectConnect, string hostLocalIP, bool useNatPunch, int port)
{
LeaveRoom(clientId);
Program.instance.NATConnections.TryGetValue(clientId, out IPEndPoint hostIP);
Room room = new Room
{
hostId = clientId,
maxPlayers = maxPlayers,
serverName = serverName,
isPublic = isPublic,
serverData = serverData,
clients = new List<int>(),
// hard coded for now REMEMBER TO UN-HARDCODE RETARD
// this is needed for load balancer to know which server this room
// belongs to
relayInfo = new RelayAddress { Address = Program.publicIP, Port = 7777, EndpointPort = Program.conf.EndpointPort },
serverId = GetRandomServerID(),
hostIP = hostIP,
hostLocalIP = hostLocalIP,
supportsDirectConnect = hostIP == null ? false : useDirectConnect,
port = port,
useNATPunch = useNatPunch
};
rooms.Add(room);
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.RoomCreated);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
void LeaveRoom(int clientId, int requiredHostId = -1)
{
for(int i = 0; i < rooms.Count; i++)
{
if(rooms[i].hostId == clientId)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft);
for(int x = 0; x < rooms[i].clients.Count; x++)
Program.transport.ServerSend(rooms[i].clients[x], 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
rooms[i].clients.Clear();
rooms.RemoveAt(i);
return;
}
else
{
if (requiredHostId >= 0 && rooms[i].hostId != requiredHostId)
continue;
if(rooms[i].clients.RemoveAll(x => x == clientId) > 0)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.PlayerDisconnected);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
}
}
}
void SendClientID(int clientId)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetID);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
int GetRandomServerID()
{
Random rand = new Random();
int temp = rand.Next(int.MinValue, int.MaxValue);
while (DoesServerIdExist(temp))
temp = rand.Next(int.MinValue, int.MaxValue);
return temp;
}
bool DoesServerIdExist(int id)
{
for (int i = 0; i < rooms.Count; i++)
if (rooms[i].serverId == id)
return true;
return false;
}
}
public enum OpCodes
{
Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10,
LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20,
DirectConnectIP = 21
}
}

View file

@ -0,0 +1,135 @@
using System;
using System.Buffers;
using System.Linq;
using System.Net;
namespace LightReflectiveMirror
{
public partial class RelayHandler
{
public RelayHandler(int maxPacketSize)
{
this._maxPacketSize = maxPacketSize;
_sendBuffers = ArrayPool<byte>.Create(maxPacketSize, 50);
}
/// <summary>
/// This is called when a client wants to send data to another player.
/// </summary>
/// <param name="clientId">The ID of the client who is sending the data</param>
/// <param name="clientData">The binary data the client is sending</param>
/// <param name="channel">The channel the client is sending this data on</param>
/// <param name="sendTo">Who to relay the data to</param>
void ProcessData(int clientId, byte[] clientData, int channel, int sendTo = -1)
{
Room playersRoom = GetRoomForPlayer(clientId);
if(playersRoom != null)
{
Room room = playersRoom;
if(room.hostId == clientId)
{
if (room.clients.Contains(sendTo))
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(_maxPacketSize);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData);
sendBuffer.WriteBytes(ref pos, clientData);
Program.transport.ServerSend(sendTo, channel, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
}
else
{
// We are not the host, so send the data to the host.
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(_maxPacketSize);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData);
sendBuffer.WriteBytes(ref pos, clientData);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(room.hostId, channel, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
}
}
/// <summary>
/// Called when a client wants to request their own ID.
/// </summary>
/// <param name="clientId">The client requesting their ID</param>
void SendClientID(int clientId)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetID);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
/// <summary>
/// Generates a random server ID.
/// </summary>
/// <returns></returns>
string GetRandomServerID()
{
if (!Program.conf.UseLoadBalancer)
{
const int LENGTH = 5;
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var randomID = "";
do
{
var random = new System.Random();
randomID = new string(Enumerable.Repeat(chars, LENGTH)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
while (DoesServerIdExist(randomID));
return randomID;
}
else
{
// ping load balancer here
using (WebClient wc = new())
{
var uri = new Uri($"http://{Program.conf.LoadBalancerAddress}:{Program.conf.LoadBalancerPort}/api/get/id");
string randomID = wc.DownloadString(uri).Replace("\\r", "").Replace("\\n", "").Trim();
return randomID;
}
}
}
/// <summary>
/// Checks if a server id already is in use.
/// </summary>
/// <param name="id">The ID to check for</param>
/// <returns></returns>
bool DoesServerIdExist(string id)
{
for (int i = 0; i < rooms.Count; i++)
if (rooms[i].serverId == id)
return true;
return false;
}
}
public enum OpCodes
{
Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10,
LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20,
DirectConnectIP = 21
}
}

View file

@ -0,0 +1,110 @@
using System;
namespace LightReflectiveMirror
{
public partial class RelayHandler
{
/// <summary>
/// Invoked when a client connects to this LRM server.
/// </summary>
/// <param name="clientId">The ID of the client who connected.</param>
public void ClientConnected(int clientId)
{
_pendingAuthentication.Add(clientId);
var buffer = _sendBuffers.Rent(1);
int pos = 0;
buffer.WriteByte(ref pos, (byte)OpCodes.AuthenticationRequest);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(buffer, 0, pos));
_sendBuffers.Return(buffer);
}
/// <summary>
/// Handles the processing of data from a client.
/// </summary>
/// <param name="clientId">The client who sent the data</param>
/// <param name="segmentData">The binary data</param>
/// <param name="channel">The channel the client sent the data on</param>
public void HandleMessage(int clientId, ArraySegment<byte> segmentData, int channel)
{
try
{
var data = segmentData.Array;
int pos = segmentData.Offset;
OpCodes opcode = (OpCodes)data.ReadByte(ref pos);
if (_pendingAuthentication.Contains(clientId))
{
if (opcode == OpCodes.AuthenticationResponse)
{
string authResponse = data.ReadString(ref pos);
if (authResponse == Program.conf.AuthenticationKey)
{
_pendingAuthentication.Remove(clientId);
int writePos = 0;
var sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref writePos, (byte)OpCodes.Authenticated);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, writePos));
}
}
return;
}
switch (opcode)
{
case OpCodes.CreateRoom:
CreateRoom(clientId, data.ReadInt(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadInt(ref pos));
break;
case OpCodes.RequestID:
SendClientID(clientId);
break;
case OpCodes.LeaveRoom:
LeaveRoom(clientId);
break;
case OpCodes.JoinServer:
JoinRoom(clientId, data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos));
break;
case OpCodes.KickPlayer:
LeaveRoom(data.ReadInt(ref pos), clientId);
break;
case OpCodes.SendData:
ProcessData(clientId, data.ReadBytes(ref pos), channel, data.ReadInt(ref pos));
break;
case OpCodes.UpdateRoomData:
var plyRoom = GetRoomForPlayer(clientId);
if (plyRoom == null)
return;
bool newName = data.ReadBool(ref pos);
if (newName)
plyRoom.serverName = data.ReadString(ref pos);
bool newData = data.ReadBool(ref pos);
if (newData)
plyRoom.serverData = data.ReadString(ref pos);
bool newPublicStatus = data.ReadBool(ref pos);
if (newPublicStatus)
plyRoom.isPublic = data.ReadBool(ref pos);
bool newPlayerCap = data.ReadBool(ref pos);
if (newPlayerCap)
plyRoom.maxPlayers = data.ReadInt(ref pos);
break;
}
}
catch
{
// Do Nothing. Client probably sent some invalid data.
}
}
/// <summary>
/// Invoked when a client disconnects from the relay.
/// </summary>
/// <param name="clientId">The ID of the client who disconnected</param>
public void HandleDisconnect(int clientId) => LeaveRoom(clientId);
}
}

View file

@ -0,0 +1,204 @@
using LightReflectiveMirror.Endpoints;
using System;
using System.Collections.Generic;
using System.Net;
namespace LightReflectiveMirror
{
public partial class RelayHandler
{
/// <summary>
/// Returns the current room the client is in, null if client is not in a room.
/// </summary>
/// <param name="clientId">The client we are getting the room for</param>
/// <returns></returns>
Room GetRoomForPlayer(int clientId)
{
for (int i = 0; i < rooms.Count; i++)
{
if (rooms[i].hostId == clientId)
return rooms[i];
if (rooms[i].clients.Contains(clientId))
return rooms[i];
}
return null;
}
/// <summary>
/// Attempts to join a room for a client.
/// </summary>
/// <param name="clientId">The client requesting to join the room</param>
/// <param name="serverId">The server ID of the room</param>
/// <param name="canDirectConnect">If the client is capable of a direct connection</param>
/// <param name="localIP">The local IP of the client joining</param>
void JoinRoom(int clientId, string serverId, bool canDirectConnect, string localIP)
{
LeaveRoom(clientId);
for (int i = 0; i < rooms.Count; i++)
{
if (rooms[i].serverId == serverId)
{
if (rooms[i].clients.Count < rooms[i].maxPlayers)
{
rooms[i].clients.Add(clientId);
int sendJoinPos = 0;
byte[] sendJoinBuffer = _sendBuffers.Rent(500);
if (canDirectConnect && Program.instance.NATConnections.ContainsKey(clientId) && rooms[i].supportsDirectConnect)
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
if (Program.instance.NATConnections[clientId].Address.Equals(rooms[i].hostIP.Address))
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostLocalIP == localIP ? "127.0.0.1" : rooms[i].hostLocalIP);
else
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostIP.Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, rooms[i].useNATPunch ? rooms[i].hostIP.Port : rooms[i].port);
sendJoinBuffer.WriteBool(ref sendJoinPos, rooms[i].useNATPunch);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
if (rooms[i].useNATPunch)
{
sendJoinPos = 0;
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
Console.WriteLine(Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteString(ref sendJoinPos, Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, Program.instance.NATConnections[clientId].Port);
sendJoinBuffer.WriteBool(ref sendJoinPos, true);
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
}
_sendBuffers.Return(sendJoinBuffer);
Endpoint.RoomsModified();
return;
}
else
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.ServerJoined);
sendJoinBuffer.WriteInt(ref sendJoinPos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
_sendBuffers.Return(sendJoinBuffer);
Endpoint.RoomsModified();
return;
}
}
}
}
// If it got to here, then the server was not found, or full. Tell the client.
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
}
/// <summary>
/// Creates a room on the LRM node.
/// </summary>
/// <param name="clientId">The client requesting to create a room</param>
/// <param name="maxPlayers">The maximum amount of players for this room</param>
/// <param name="serverName">The name for the server</param>
/// <param name="isPublic">Weather or not the server should show up on the server list</param>
/// <param name="serverData">Extra data the host can include</param>
/// <param name="useDirectConnect">Weather or not, the host is capable of doing direct connections</param>
/// <param name="hostLocalIP">The hosts local IP</param>
/// <param name="useNatPunch">Weather or not, the host is supporting NAT Punch</param>
/// <param name="port">The port of the direct connect transport on the host</param>
void CreateRoom(int clientId, int maxPlayers, string serverName, bool isPublic, string serverData, bool useDirectConnect, string hostLocalIP, bool useNatPunch, int port)
{
LeaveRoom(clientId);
Program.instance.NATConnections.TryGetValue(clientId, out IPEndPoint hostIP);
Room room = new Room
{
hostId = clientId,
maxPlayers = maxPlayers,
serverName = serverName,
isPublic = isPublic,
serverData = serverData,
clients = new List<int>(),
relayInfo = new RelayAddress { address = Program.publicIP, port = Program.conf.TransportPort, endpointPort = Program.conf.EndpointPort },
serverId = GetRandomServerID(),
hostIP = hostIP,
hostLocalIP = hostLocalIP,
supportsDirectConnect = hostIP == null ? false : useDirectConnect,
port = port,
useNATPunch = useNatPunch
};
rooms.Add(room);
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.RoomCreated);
sendBuffer.WriteString(ref pos, room.serverId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
Endpoint.RoomsModified();
}
/// <summary>
/// Makes the client leave their room.
/// </summary>
/// <param name="clientId">The client of which to remove from their room</param>
/// <param name="requiredHostId">The ID of the client who kicked the client. -1 if the client left on their own terms</param>
void LeaveRoom(int clientId, int requiredHostId = -1)
{
for (int i = 0; i < rooms.Count; i++)
{
if (rooms[i].hostId == clientId)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(1);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft);
for (int x = 0; x < rooms[i].clients.Count; x++)
Program.transport.ServerSend(rooms[i].clients[x], 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
rooms[i].clients.Clear();
rooms.RemoveAt(i);
Endpoint.RoomsModified();
return;
}
else
{
if (requiredHostId >= 0 && rooms[i].hostId != requiredHostId)
continue;
if (rooms[i].clients.RemoveAll(x => x == clientId) > 0)
{
int pos = 0;
byte[] sendBuffer = _sendBuffers.Rent(5);
sendBuffer.WriteByte(ref pos, (byte)OpCodes.PlayerDisconnected);
sendBuffer.WriteInt(ref pos, clientId);
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendBuffer, 0, pos));
_sendBuffers.Return(sendBuffer);
Endpoint.RoomsModified();
}
}
}
}
}
}

View file

@ -0,0 +1,13 @@
using System.Buffers;
using System.Collections.Generic;
namespace LightReflectiveMirror
{
public partial class RelayHandler
{
public List<Room> rooms = new List<Room>();
private List<int> _pendingAuthentication = new List<int>();
private ArrayPool<byte> _sendBuffers;
private int _maxPacketSize = 0;
}
}

View file

@ -8,7 +8,7 @@ namespace LightReflectiveMirror
[JsonObject(MemberSerialization.OptOut)] [JsonObject(MemberSerialization.OptOut)]
public class Room public class Room
{ {
public int serverId; public string serverId;
public int hostId; public int hostId;
public string serverName; public string serverName;
public string serverData; public string serverData;
@ -33,8 +33,8 @@ namespace LightReflectiveMirror
[Serializable] [Serializable]
public struct RelayAddress public struct RelayAddress
{ {
public ushort Port; public ushort port;
public ushort EndpointPort; public ushort endpointPort;
public string Address; public string address;
} }
} }

View file

@ -159,7 +159,7 @@ namespace Mirror
/// <summary> /// <summary>
/// Start listening for clients /// Start listening for clients
/// </summary> /// </summary>
public abstract void ServerStart(); public abstract void ServerStart(ushort port);
/// <summary> /// <summary>
/// Send data to a client. /// Send data to a client.

Binary file not shown.

View file

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace kcp2k
{
class KCPConfig
{
public bool NoDelay = true;
public uint Interval = 10;
public int FastResend = 2;
public bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
public int ConnectionTimeout = 10000; // Time in miliseconds it takes for a connection to time out.
}
}

View file

@ -0,0 +1,237 @@
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
using System;
using System.IO;
using System.Linq;
using System.Net;
using Mirror;
using Newtonsoft.Json;
namespace kcp2k
{
public class KcpTransport : Transport
{
// scheme used by this transport
public const string Scheme = "kcp";
// common
public static int ConnectionTimeout = 10000;
public bool NoDelay = true;
public uint Interval = 10;
public int FastResend = 2;
public bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
// server & client
KcpServer server;
KcpClient client;
// debugging
public bool debugLog;
// show statistics in OnGUI
public bool statisticsGUI;
// log statistics for headless servers that can't show them in GUI
public bool statisticsLog;
void Awake()
{
KCPConfig conf = new KCPConfig();
if (!File.Exists("KCPConfig.json"))
{
File.WriteAllText("KCPConfig.json", JsonConvert.SerializeObject(conf, Formatting.Indented));
}
else
{
conf = JsonConvert.DeserializeObject<KCPConfig>(File.ReadAllText("KCPConfig.json"));
}
NoDelay = conf.NoDelay;
Interval = conf.Interval;
FastResend = conf.FastResend;
CongestionWindow = conf.CongestionWindow;
SendWindowSize = conf.SendWindowSize;
ReceiveWindowSize = conf.ReceiveWindowSize;
ConnectionTimeout = conf.ConnectionTimeout;
// logging
// Log.Info should use Debug.Log if enabled, or nothing otherwise
// (don't want to spam the console on headless servers)
if (debugLog)
Log.Info = Console.WriteLine;
else
Log.Info = _ => { };
Log.Warning = Console.WriteLine;
Log.Error = Console.WriteLine;
// client
client = new KcpClient(
() => OnClientConnected.Invoke(),
(message) => OnClientDataReceived.Invoke(message, 0),
() => OnClientDisconnected.Invoke()
);
// server
server = new KcpServer(
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, 0),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
NoDelay,
Interval,
FastResend,
CongestionWindow,
SendWindowSize,
ReceiveWindowSize
);
Console.WriteLine("KcpTransport initialized!");
}
// all except WebGL
public override bool Available() => true;
// client
public override bool ClientConnected() => client.connected;
public override void ClientConnect(string address) { }
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case 1:
client.Send(segment, KcpChannel.Unreliable);
break;
default:
client.Send(segment, KcpChannel.Reliable);
break;
}
}
public override void ClientDisconnect() => client.Disconnect();
// scene change message will disable transports.
// kcp processes messages in an internal loop which should be
// stopped immediately after scene change (= after disabled)
// => kcp has tests to guaranteed that calling .Pause() during the
// receive loop stops the receive loop immediately, not after.
void OnEnable()
{
// unpause when enabled again
client?.Unpause();
server?.Unpause();
}
void OnDisable()
{
// pause immediately when not enabled anymore
client?.Pause();
server?.Pause();
}
// server
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder();
builder.Scheme = Scheme;
builder.Host = Dns.GetHostName();
return builder.Uri;
}
public override bool ServerActive() => server.IsActive();
public override void ServerStart(ushort requestedPort) => server.Start(requestedPort);
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case 1:
server.Send(connectionId, segment, KcpChannel.Unreliable);
break;
default:
server.Send(connectionId, segment, KcpChannel.Reliable);
break;
}
}
public override bool ServerDisconnect(int connectionId)
{
server.Disconnect(connectionId);
return true;
}
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
public override void ServerStop() => server.Stop();
public void Update()
{
server.TickIncoming();
server.TickOutgoing();
}
// common
public override void Shutdown() {}
// max message size
public override int GetMaxPacketSize(int channelId = 0)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case 1:
return KcpConnection.UnreliableMaxMessageSize;
default:
return KcpConnection.ReliableMaxMessageSize;
}
}
// server statistics
public int GetAverageMaxSendRate() =>
server.connections.Count > 0
? server.connections.Values.Sum(conn => (int)conn.MaxSendRate) / server.connections.Count
: 0;
public int GetAverageMaxReceiveRate() =>
server.connections.Count > 0
? server.connections.Values.Sum(conn => (int)conn.MaxReceiveRate) / server.connections.Count
: 0;
int GetTotalSendQueue() =>
server.connections.Values.Sum(conn => conn.SendQueueCount);
int GetTotalReceiveQueue() =>
server.connections.Values.Sum(conn => conn.ReceiveQueueCount);
int GetTotalSendBuffer() =>
server.connections.Values.Sum(conn => conn.SendBufferCount);
int GetTotalReceiveBuffer() =>
server.connections.Values.Sum(conn => conn.ReceiveBufferCount);
// PrettyBytes function from DOTSNET
// pretty prints bytes as KB/MB/GB/etc.
// long to support > 2GB
// divides by floats to return "2.5MB" etc.
public static string PrettyBytes(long bytes)
{
// bytes
if (bytes < 1024)
return $"{bytes} B";
// kilobytes
else if (bytes < 1024L * 1024L)
return $"{(bytes / 1024f):F2} KB";
// megabytes
else if (bytes < 1024 * 1024L * 1024L)
return $"{(bytes / (1024f * 1024f)):F2} MB";
// gigabytes
return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB";
}
public override string ToString() => "KCP";
}
}
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet

View file

@ -0,0 +1,10 @@
namespace kcp2k
{
// channel type and header for raw messages
public enum KcpChannel : byte
{
// don't react on 0x00. might help to filter out random noise.
Reliable = 0x01,
Unreliable = 0x02
}
}

View file

@ -0,0 +1,114 @@
// kcp client logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
namespace kcp2k
{
public class KcpClient
{
// events
public Action OnConnected;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// state
public KcpClientConnection connection;
public bool connected;
public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
}
public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
if (connected)
{
Log.Warning("KCP: client already connected!");
return;
}
connection = new KcpClientConnection();
// setup events
connection.OnAuthenticated = () =>
{
Log.Info($"KCP: OnClientConnected");
connected = true;
OnConnected.Invoke();
};
connection.OnData = (message) =>
{
//Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(message);
};
connection.OnDisconnected = () =>
{
Log.Info($"KCP: OnClientDisconnected");
connected = false;
connection = null;
OnDisconnected.Invoke();
};
// connect
connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
}
public void Send(ArraySegment<byte> segment, KcpChannel channel)
{
if (connected)
{
connection.SendData(segment, channel);
}
else Log.Warning("KCP: can't send because client not connected!");
}
public void Disconnect()
{
// only if connected
// otherwise we end up in a deadlock because of an open Mirror bug:
// https://github.com/vis2k/Mirror/issues/2353
if (connected)
{
// call Disconnect and let the connection handle it.
// DO NOT set it to null yet. it needs to be updated a few more
// times first. let the connection handle it!
connection?.Disconnect();
}
}
// process incoming messages. should be called before updating the world.
public void TickIncoming()
{
// recv on socket first, then process incoming
// (even if we didn't receive anything. need to tick ping etc.)
// (connection is null if not active)
connection?.RawReceive();
connection?.TickIncoming();
}
// process outgoing messages. should be called after updating the world.
public void TickOutgoing()
{
// process outgoing
// (connection is null if not active)
connection?.TickOutgoing();
}
// process incoming and outgoing for convenience
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public void Tick()
{
TickIncoming();
TickOutgoing();
}
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause() => connection?.Pause();
public void Unpause() => connection?.Unpause();
}
}

View file

@ -0,0 +1,75 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpClientConnection : KcpConnection
{
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
// if MaxMessageSize is larger. kcp always sends in MTU
// segments and having a buffer smaller than MTU would
// silently drop excess data.
// => we need the MTU to fit channel + message!
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
Log.Info($"KcpClient: connect to {host}:{port}");
IPAddress[] ipAddress = Dns.GetHostAddresses(host);
if (ipAddress.Length < 1)
throw new SocketException((int)SocketError.HostNotFound);
remoteEndpoint = new IPEndPoint(ipAddress[0], port);
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
socket.Connect(remoteEndpoint);
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
// client should send handshake to server as very first message
SendHandshake();
RawReceive();
}
// call from transport update
public void RawReceive()
{
try
{
if (socket != null)
{
while (socket.Poll(0, SelectMode.SelectRead))
{
int msgLength = socket.ReceiveFrom(rawReceiveBuffer, ref remoteEndpoint);
// IMPORTANT: detect if buffer was too small for the
// received msgLength. otherwise the excess
// data would be silently lost.
// (see ReceiveFrom documentation)
if (msgLength <= rawReceiveBuffer.Length)
{
//Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
RawInput(rawReceiveBuffer, msgLength);
}
else
{
Log.Error($"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
Disconnect();
}
}
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
}
protected override void Dispose()
{
socket.Close();
socket = null;
}
protected override void RawSend(byte[] data, int length)
{
socket.Send(data, length, SocketFlags.None);
}
}
}

View file

@ -0,0 +1,667 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
enum KcpState { Connected, Authenticated, Disconnected }
public abstract class KcpConnection
{
protected Socket socket;
protected EndPoint remoteEndpoint;
internal Kcp kcp;
// kcp can have several different states, let's use a state machine
KcpState state = KcpState.Disconnected;
public Action OnAuthenticated;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// Mirror needs a way to stop the kcp message processing while loop
// immediately after a scene change message. Mirror can't process any
// other messages during a scene change.
// (could be useful for others too)
bool paused;
uint lastReceiveTime;
// internal time.
// StopWatch offers ElapsedMilliSeconds and should be more precise than
// Unity's time.deltaTime over long periods.
readonly Stopwatch refTime = new Stopwatch();
// we need to subtract the channel byte from every MaxMessageSize
// calculation.
// we also need to tell kcp to use MTU-1 to leave space for the byte.
const int CHANNEL_HEADER_SIZE = 1;
// reliable channel (= kcp) MaxMessageSize so the outside knows largest
// allowed message to send the calculation in Send() is not obvious at
// all, so let's provide the helper here.
//
// kcp does fragmentation, so max message is way larger than MTU.
//
// -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD
// -> Send() checks if fragment count < WND_RCV, so we use WND_RCV - 1.
// note that Send() checks WND_RCV instead of wnd_rcv which may or
// may not be a bug in original kcp. but since it uses the define, we
// can use that here too.
// -> we add 1 byte KcpHeader enum to each message, so -1
//
// IMPORTANT: max message is MTU * WND_RCV, in other words it completely
// fills the receive window! due to head of line blocking,
// all other messages have to wait while a maxed size message
// is being delivered.
// => in other words, DO NOT use max size all the time like
// for batching.
// => sending UNRELIABLE max message size most of the time is
// best for performance (use that one for batching!)
public const int ReliableMaxMessageSize = (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * (Kcp.WND_RCV - 1) - 1;
// unreliable max message size is simply MTU - channel header size
public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE;
// buffer to receive kcp's processed messages (avoids allocations).
// IMPORTANT: this is for KCP messages. so it needs to be of size:
// 1 byte header + MaxMessageSize content
byte[] kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize];
// send buffer for handing user messages to kcp for processing.
// (avoids allocations).
// IMPORTANT: needs to be of size:
// 1 byte header + MaxMessageSize content
byte[] kcpSendBuffer = new byte[1 + ReliableMaxMessageSize];
// raw send buffer is exactly MTU.
byte[] rawSendBuffer = new byte[Kcp.MTU_DEF];
// send a ping occasionally so we don't time out on the other end.
// for example, creating a character in an MMO could easily take a
// minute of no data being sent. which doesn't mean we want to time out.
// same goes for slow paced card games etc.
public const int PING_INTERVAL = 1000;
uint lastPingTime;
// if we send more than kcp can handle, we will get ever growing
// send/recv buffers and queues and minutes of latency.
// => if a connection can't keep up, it should be disconnected instead
// to protect the server under heavy load, and because there is no
// point in growing to gigabytes of memory or minutes of latency!
// => 2k isn't enough. we reach 2k when spawning 4k monsters at once
// easily, but it does recover over time.
// => 10k seems safe.
//
// note: we have a ChokeConnectionAutoDisconnects test for this too!
internal const int QueueDisconnectThreshold = 10000;
// getters for queue and buffer counts, used for debug info
public int SendQueueCount => kcp.snd_queue.Count;
public int ReceiveQueueCount => kcp.rcv_queue.Count;
public int SendBufferCount => kcp.snd_buf.Count;
public int ReceiveBufferCount => kcp.rcv_buf.Count;
// maximum send rate per second can be calculated from kcp parameters
// source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html
//
// KCP can send/receive a maximum of WND*MTU per interval.
// multiple by 1000ms / interval to get the per-second rate.
//
// example:
// WND(32) * MTU(1400) = 43.75KB
// => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s
//
// returns bytes/second!
public uint MaxSendRate =>
kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval;
public uint MaxReceiveRate =>
kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval;
// NoDelay, interval, window size are the most important configurations.
// let's force require the parameters so we don't forget it anywhere.
protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
// set up kcp over reliable channel (that's what kcp is for)
kcp = new Kcp(0, RawSendReliable);
// set nodelay.
// note that kcp uses 'nocwnd' internally so we negate the parameter
kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow);
kcp.SetWindowSize(sendWindowSize, receiveWindowSize);
// IMPORTANT: high level needs to add 1 channel byte to each raw
// message. so while Kcp.MTU_DEF is perfect, we actually need to
// tell kcp to use MTU-1 so we can still put the header into the
// message afterwards.
kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE);
state = KcpState.Connected;
refTime.Start();
}
void HandleTimeout(uint time)
{
// note: we are also sending a ping regularly, so timeout should
// only ever happen if the connection is truly gone.
if (time >= lastReceiveTime + KcpTransport.ConnectionTimeout)
{
Log.Warning($"KCP: Connection timed out after not receiving any message for {KcpTransport.ConnectionTimeout}ms. Disconnecting.");
Disconnect();
}
}
void HandleDeadLink()
{
// kcp has 'dead_link' detection. might as well use it.
if (kcp.state == -1)
{
Log.Warning("KCP Connection dead_link detected. Disconnecting.");
Disconnect();
}
}
// send a ping occasionally in order to not time out on the other end.
void HandlePing(uint time)
{
// enough time elapsed since last ping?
if (time >= lastPingTime + PING_INTERVAL)
{
// ping again and reset time
//Log.Debug("KCP: sending ping...");
SendPing();
lastPingTime = time;
}
}
void HandleChoked()
{
// disconnect connections that can't process the load.
// see QueueSizeDisconnect comments.
// => include all of kcp's buffers and the unreliable queue!
int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
kcp.rcv_buf.Count + kcp.snd_buf.Count;
if (total >= QueueDisconnectThreshold)
{
Log.Warning($"KCP: disconnecting connection because it can't process data fast enough.\n" +
$"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
$"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
$"* Or perhaps the network is simply too slow on our end, or on the other end.\n");
// let's clear all pending sends before disconnting with 'Bye'.
// otherwise a single Flush in Disconnect() won't be enough to
// flush thousands of messages to finally deliver 'Bye'.
// this is just faster and more robust.
kcp.snd_queue.Clear();
Disconnect();
}
}
// reads the next reliable message type & content from kcp.
// -> to avoid buffering, unreliable messages call OnData directly.
bool ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message)
{
int msgSize = kcp.PeekSize();
message = new ArraySegment<byte>();
if (msgSize > 0)
{
// only allow receiving up to buffer sized messages.
// otherwise we would get BlockCopy ArgumentException anyway.
if (msgSize <= kcpMessageBuffer.Length)
{
// receive from kcp
int received = kcp.Receive(kcpMessageBuffer, msgSize);
if (received >= 0)
{
// extract header & content without header
header = (KcpHeader)kcpMessageBuffer[0];
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
return true;
}
else
{
// if receive failed, close everything
Log.Warning($"Receive failed with error={received}. closing connection.");
Disconnect();
}
}
// we don't allow sending messages > Max, so this must be an
// attacker. let's disconnect to avoid allocation attacks etc.
else
{
Log.Warning($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
Disconnect();
}
}
header = KcpHeader.Disconnect;
return false;
}
void TickIncoming_Connected(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// any reliable kcp message received?
if (ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
{
// we were waiting for a handshake.
// it proves that the other end speaks our protocol.
Log.Info("KCP: received handshake");
state = KcpState.Authenticated;
OnAuthenticated?.Invoke();
break;
}
case KcpHeader.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
case KcpHeader.Data:
case KcpHeader.Disconnect:
{
// everything else is not allowed during handshake!
Log.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
Disconnect();
break;
}
}
}
}
void TickIncoming_Authenticated(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// process all received messages
//
// Mirror scene changing requires transports to immediately stop
// processing any more messages after a scene message was
// received. and since we are in a while loop here, we need this
// extra check.
//
// note while that this is mainly for Mirror, but might be
// useful in other applications too.
//
// note that we check it BEFORE ever calling ReceiveNext. otherwise
// we would silently eat the received message and never process it.
while (!paused &&
ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
{
// should never receive another handshake after auth
Log.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection.");
Disconnect();
break;
}
case KcpHeader.Data:
{
// call OnData IF the message contained actual data
if (message.Count > 0)
{
//Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}");
OnData?.Invoke(message);
}
// empty data = attacker, or something went wrong
else
{
Log.Warning("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
Disconnect();
}
break;
}
case KcpHeader.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
case KcpHeader.Disconnect:
{
// disconnect might happen
Log.Info("KCP: received disconnect message");
Disconnect();
break;
}
}
}
}
public void TickIncoming()
{
uint time = (uint)refTime.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
{
TickIncoming_Connected(time);
break;
}
case KcpState.Authenticated:
{
TickIncoming_Authenticated(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
{
// unexpected
Log.Error(ex.ToString());
Disconnect();
}
}
public void TickOutgoing()
{
uint time = (uint)refTime.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
case KcpState.Authenticated:
{
// update flushes out messages
kcp.Update(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
{
// unexpected
Log.Error(ex.ToString());
Disconnect();
}
}
public void RawInput(byte[] buffer, int msgLength)
{
// parse channel
if (msgLength > 0)
{
byte channel = buffer[0];
switch (channel)
{
case (byte)KcpChannel.Reliable:
{
// input into kcp, but skip channel byte
int input = kcp.Input(buffer, 1, msgLength - 1);
if (input != 0)
{
Log.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}");
}
break;
}
case (byte)KcpChannel.Unreliable:
{
// ideally we would queue all unreliable messages and
// then process them in ReceiveNext() together with the
// reliable messages, but:
// -> queues/allocations/pools are slow and complex.
// -> DOTSNET 10k is actually slower if we use pooled
// unreliable messages for transform messages.
//
// DOTSNET 10k benchmark:
// reliable-only: 170 FPS
// unreliable queued: 130-150 FPS
// unreliable direct: 183 FPS(!)
//
// DOTSNET 50k benchmark:
// reliable-only: FAILS (queues keep growing)
// unreliable direct: 18-22 FPS(!)
//
// -> all unreliable messages are DATA messages anyway.
// -> let's skip the magic and call OnData directly if
// the current state allows it.
if (state == KcpState.Authenticated)
{
// only process messages while not paused for Mirror
// scene switching etc.
// -> if an unreliable message comes in while
// paused, simply drop it. it's unreliable!
if (!paused)
{
ArraySegment<byte> message = new ArraySegment<byte>(buffer, 1, msgLength - 1);
OnData?.Invoke(message);
}
// set last receive time to avoid timeout.
// -> we do this in ANY case even if not enabled.
// a message is a message.
// -> we set last receive time for both reliable and
// unreliable messages. both count.
// otherwise a connection might time out even
// though unreliable were received, but no
// reliable was received.
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
}
else
{
// should never
Log.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
Disconnect();
}
break;
}
default:
{
// not a valid channel. random data or attacks.
Log.Info($"Disconnecting connection because of invalid channel header: {channel}");
Disconnect();
break;
}
}
}
}
// raw send puts the data into the socket
protected abstract void RawSend(byte[] data, int length);
// raw send called by kcp
void RawSendReliable(byte[] data, int length)
{
// copy channel header, data into raw send buffer, then send
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length);
RawSend(rawSendBuffer, length + 1);
}
void SendReliable(KcpHeader header, ArraySegment<byte> content)
{
// 1 byte header + content needs to fit into send buffer
if (1 + content.Count <= kcpSendBuffer.Length) // TODO
{
// copy header, content (if any) into send buffer
kcpSendBuffer[0] = (byte)header;
if (content.Count > 0)
Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count);
// send to kcp for processing
int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count);
if (sent < 0)
{
Log.Warning($"Send failed with error={sent} for content with length={content.Count}");
}
}
// otherwise content is larger than MaxMessageSize. let user know!
else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize}");
}
void SendUnreliable(ArraySegment<byte> message)
{
// message size needs to be <= unreliable max size
if (message.Count <= UnreliableMaxMessageSize)
{
// copy channel header, data into raw send buffer, then send
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
Buffer.BlockCopy(message.Array, 0, rawSendBuffer, 1, message.Count);
RawSend(rawSendBuffer, message.Count + 1);
}
// otherwise content is larger than MaxMessageSize. let user know!
else Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}");
}
// server & client need to send handshake at different times, so we need
// to expose the function.
// * client should send it immediately.
// * server should send it as reply to client's handshake, not before
// (server should not reply to random internet messages with handshake)
// => handshake info needs to be delivered, so it goes over reliable.
public void SendHandshake()
{
Log.Info("KcpConnection: sending Handshake to other end!");
SendReliable(KcpHeader.Handshake, default);
}
public void SendData(ArraySegment<byte> data, KcpChannel channel)
{
// sending empty segments is not allowed.
// nobody should ever try to send empty data.
// it means that something went wrong, e.g. in Mirror/DOTSNET.
// let's make it obvious so it's easy to debug.
if (data.Count == 0)
{
Log.Warning("KcpConnection: tried sending empty message. This should never happen. Disconnecting.");
Disconnect();
return;
}
switch (channel)
{
case KcpChannel.Reliable:
SendReliable(KcpHeader.Data, data);
break;
case KcpChannel.Unreliable:
SendUnreliable(data);
break;
}
}
// ping goes through kcp to keep it from timing out, so it goes over the
// reliable channel.
void SendPing() => SendReliable(KcpHeader.Ping, default);
// disconnect info needs to be delivered, so it goes over reliable
void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default);
protected virtual void Dispose() {}
// disconnect this connection
public void Disconnect()
{
// only if not disconnected yet
if (state == KcpState.Disconnected)
return;
// send a disconnect message
if (socket.Connected)
{
try
{
SendDisconnect();
kcp.Flush();
}
catch (SocketException)
{
// this is ok, the connection was already closed
}
catch (ObjectDisposedException)
{
// this is normal when we stop the server
// the socket is stopped so we can't send anything anymore
// to the clients
// the clients will eventually timeout and realize they
// were disconnected
}
}
// set as Disconnected, call event
Log.Info("KCP Connection: Disconnected.");
state = KcpState.Disconnected;
OnDisconnected?.Invoke();
}
// get remote endpoint
public EndPoint GetRemoteEndPoint() => remoteEndpoint;
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause() => paused = true;
public void Unpause()
{
// unpause
paused = false;
// reset the timeout.
// we have likely been paused for > timeout seconds, but that
// doesn't mean we should disconnect. for example, Mirror pauses
// kcp during scene changes which could easily take > 10s timeout:
// see also: https://github.com/vis2k/kcp2k/issues/8
// => Unpause completely resets the timeout instead of restoring the
// time difference when we started pausing. it's more simple and
// it's a good idea to start counting from 0 after we unpaused!
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
}
}
}

View file

@ -0,0 +1,19 @@
namespace kcp2k
{
// header for messages processed by kcp.
// this is NOT for the raw receive messages(!) because handshake/disconnect
// need to be sent reliably. it's not enough to have those in rawreceive
// because those messages might get lost without being resent!
public enum KcpHeader : byte
{
// don't react on 0x00. might help to filter out random noise.
Handshake = 0x01,
// ping goes over reliable & KcpHeader for now. could go over reliable
// too. there is no real difference except that this is easier because
// we already have a KcpHeader for reliable messages.
// ping is only used to keep it alive, so latency doesn't matter.
Ping = 0x02,
Data = 0x03,
Disconnect = 0x04
}
}

View file

@ -0,0 +1,297 @@
// kcp server logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServer
{
// events
public Action<int> OnConnected;
public Action<int, ArraySegment<byte>> OnData;
public Action<int> OnDisconnected;
// configuration
// NoDelay is recommended to reduce latency. This also scales better
// without buffers getting full.
public bool NoDelay;
// KCP internal update interval. 100ms is KCP default, but a lower
// interval is recommended to minimize latency and to scale to more
// networked entities.
public uint Interval;
// KCP fastresend parameter. Faster resend for the cost of higher
// bandwidth.
public int FastResend;
// KCP 'NoCongestionWindow' is false by default. here we negate it for
// ease of use. This can be disabled for high scale games if connections
// choke regularly.
public bool CongestionWindow;
// KCP window size can be modified to support higher loads.
// for example, Mirror Benchmark requires:
// 128, 128 for 4k monsters
// 512, 512 for 10k monsters
// 8192, 8192 for 20k monsters
public uint SendWindowSize;
public uint ReceiveWindowSize;
// state
Socket socket;
#if UNITY_SWITCH
// switch does not support ipv6
EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
#else
EndPoint newClientEP = new IPEndPoint(IPAddress.IPv6Any, 0);
#endif
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
// if MaxMessageSize is larger. kcp always sends in MTU
// segments and having a buffer smaller than MTU would
// silently drop excess data.
// => we need the mtu to fit channel + message!
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
public KcpServer(Action<int> OnConnected,
Action<int, ArraySegment<byte>> OnData,
Action<int> OnDisconnected,
bool NoDelay,
uint Interval,
int FastResend = 0,
bool CongestionWindow = true,
uint SendWindowSize = Kcp.WND_SND,
uint ReceiveWindowSize = Kcp.WND_RCV)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.NoDelay = NoDelay;
this.Interval = Interval;
this.FastResend = FastResend;
this.CongestionWindow = CongestionWindow;
this.SendWindowSize = SendWindowSize;
this.ReceiveWindowSize = ReceiveWindowSize;
}
public bool IsActive() => socket != null;
public void Start(ushort port)
{
// only start once
if (socket != null)
{
Log.Warning("KCP: server already started!");
}
// listen
#if UNITY_SWITCH
// Switch does not support ipv6
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(IPAddress.Any, port));
#else
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
socket.DualMode = true;
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
#endif
}
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.SendData(segment, channel);
}
}
public void Disconnect(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Disconnect();
}
}
public string GetClientAddress(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
}
return "";
}
// process incoming messages. should be called before updating the world.
HashSet<int> connectionsToRemove = new HashSet<int>();
public void TickIncoming()
{
while (socket != null && socket.Poll(0, SelectMode.SelectRead))
{
try
{
int msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP);
//Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// calculate connectionId from endpoint
int connectionId = newClientEP.GetHashCode();
// IMPORTANT: detect if buffer was too small for the received
// msgLength. otherwise the excess data would be
// silently lost.
// (see ReceiveFrom documentation)
if (msgLength <= rawReceiveBuffer.Length)
{
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// create a new KcpConnection
connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize);
// DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either:
// * random data from the internet
// * or from a client connection that we just disconnected
// but that hasn't realized it yet, still sending data
// from last session that we should absolutely ignore.
//
//
// TODO this allocates a new KcpConnection for each new
// internet connection. not ideal, but C# UDP Receive
// already allocated anyway.
//
// expecting a MAGIC byte[] would work, but sending the raw
// UDP message without kcp's reliability will have low
// probability of being received.
//
// for now, this is fine.
// setup authenticated event that also adds to connections
connection.OnAuthenticated = () =>
{
// only send handshake to client AFTER we received his
// handshake in OnAuthenticated.
// we don't want to reply to random internet messages
// with handshakes each time.
connection.SendHandshake();
// add to connections dict after being authenticated.
connections.Add(connectionId, connection);
Log.Info($"KCP: server added connection({connectionId}): {newClientEP}");
// setup Data + Disconnected events only AFTER the
// handshake. we don't want to fire OnServerDisconnected
// every time we receive invalid random data from the
// internet.
// setup data event
connection.OnData = (message) =>
{
// call mirror event
//Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(connectionId, message);
};
// setup disconnected event
connection.OnDisconnected = () =>
{
// flag for removal
// (can't remove directly because connection is updated
// and event is called while iterating all connections)
connectionsToRemove.Add(connectionId);
// call mirror event
Log.Info($"KCP: OnServerDisconnected({connectionId})");
OnDisconnected.Invoke(connectionId);
};
// finally, call mirror OnConnected event
Log.Info($"KCP: OnServerConnected({connectionId})");
OnConnected.Invoke(connectionId);
};
// now input the message & process received ones
// connected event was set up.
// tick will process the first message and adds the
// connection if it was the handshake.
connection.RawInput(rawReceiveBuffer, msgLength);
connection.TickIncoming();
// again, do not add to connections.
// if the first message wasn't the kcp handshake then
// connection will simply be garbage collected.
}
// existing connection: simply input the message into kcp
else
{
connection.RawInput(rawReceiveBuffer, msgLength);
}
}
else
{
Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}.");
Disconnect(connectionId);
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
}
// process inputs for all server connections
// (even if we didn't receive anything. need to tick ping etc.)
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickIncoming();
}
// remove disconnected connections
// (can't do it in connection.OnDisconnected because Tick is called
// while iterating connections)
foreach (int connectionId in connectionsToRemove)
{
connections.Remove(connectionId);
}
connectionsToRemove.Clear();
}
// process outgoing messages. should be called after updating the world.
public void TickOutgoing()
{
// flush all server connections
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickOutgoing();
}
}
// process incoming and outgoing for convenience.
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public void Tick()
{
TickIncoming();
TickOutgoing();
}
public void Stop()
{
socket?.Close();
socket = null;
}
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause()
{
foreach (KcpServerConnection connection in connections.Values)
connection.Pause();
}
public void Unpause()
{
foreach (KcpServerConnection connection in connections.Values)
connection.Unpause();
}
}
}

View file

@ -0,0 +1,20 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServerConnection : KcpConnection
{
public KcpServerConnection(Socket socket, EndPoint remoteEndpoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV)
{
this.socket = socket;
this.remoteEndpoint = remoteEndpoint;
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize);
}
protected override void RawSend(byte[] data, int length)
{
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndpoint);
}
}
}

View file

@ -0,0 +1,14 @@
// A simple logger class that uses Console.WriteLine by default.
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
// (this way we don't have to depend on UnityEngine)
using System;
namespace kcp2k
{
public static class Log
{
public static Action<string> Info = Console.WriteLine;
public static Action<string> Warning = Console.WriteLine;
public static Action<string> Error = Console.Error.WriteLine;
}
}

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("kcp2k.Tests")]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.IO;
namespace kcp2k
{
// KCP Segment Definition
internal class Segment
{
internal uint conv; // conversation
internal uint cmd; // command, e.g. Kcp.CMD_ACK etc.
internal uint frg; // fragment
internal uint wnd; // window size that the receive can currently receive
internal uint ts; // timestamp
internal uint sn; // serial number
internal uint una;
internal uint resendts; // resend timestamp
internal int rto;
internal uint fastack;
internal uint xmit;
// we need a auto scaling byte[] with a WriteBytes function.
// MemoryStream does that perfectly, no need to reinvent the wheel.
// note: no need to pool it, because Segment is already pooled.
internal MemoryStream data = new MemoryStream();
// pool ////////////////////////////////////////////////////////////////
internal static readonly Stack<Segment> Pool = new Stack<Segment>(32);
public static Segment Take()
{
if (Pool.Count > 0)
{
Segment seg = Pool.Pop();
return seg;
}
return new Segment();
}
public static void Return(Segment seg)
{
seg.Reset();
Pool.Push(seg);
}
////////////////////////////////////////////////////////////////////////
// ikcp_encode_seg
// encode a segment into buffer
internal int Encode(byte[] ptr, int offset)
{
int offset_ = offset;
offset += Utils.Encode32U(ptr, offset, conv);
offset += Utils.Encode8u(ptr, offset, (byte)cmd);
offset += Utils.Encode8u(ptr, offset, (byte)frg);
offset += Utils.Encode16U(ptr, offset, (ushort)wnd);
offset += Utils.Encode32U(ptr, offset, ts);
offset += Utils.Encode32U(ptr, offset, sn);
offset += Utils.Encode32U(ptr, offset, una);
offset += Utils.Encode32U(ptr, offset, (uint)data.Position);
return offset - offset_;
}
// reset to return a fresh segment to the pool
internal void Reset()
{
conv = 0;
cmd = 0;
frg = 0;
wnd = 0;
ts = 0;
sn = 0;
una = 0;
rto = 0;
xmit = 0;
resendts = 0;
fastack = 0;
// keep buffer for next pool usage, but reset length (= bytes written)
data.SetLength(0);
}
}
}

View file

@ -0,0 +1,76 @@
using System.Runtime.CompilerServices;
namespace kcp2k
{
public static partial class Utils
{
// Clamp so we don't have to depend on UnityEngine
public static int Clamp(int value, int min, int max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
// encode 8 bits unsigned int
public static int Encode8u(byte[] p, int offset, byte c)
{
p[0 + offset] = c;
return 1;
}
// decode 8 bits unsigned int
public static int Decode8u(byte[] p, int offset, ref byte c)
{
c = p[0 + offset];
return 1;
}
// encode 16 bits unsigned int (lsb)
public static int Encode16U(byte[] p, int offset, ushort w)
{
p[0 + offset] = (byte)(w >> 0);
p[1 + offset] = (byte)(w >> 8);
return 2;
}
// decode 16 bits unsigned int (lsb)
public static int Decode16U(byte[] p, int offset, ref ushort c)
{
ushort result = 0;
result |= p[0 + offset];
result |= (ushort)(p[1 + offset] << 8);
c = result;
return 2;
}
// encode 32 bits unsigned int (lsb)
public static int Encode32U(byte[] p, int offset, uint l)
{
p[0 + offset] = (byte)(l >> 0);
p[1 + offset] = (byte)(l >> 8);
p[2 + offset] = (byte)(l >> 16);
p[3 + offset] = (byte)(l >> 24);
return 4;
}
// decode 32 bits unsigned int (lsb)
public static int Decode32U(byte[] p, int offset, ref uint c)
{
uint result = 0;
result |= p[0 + offset];
result |= (uint)(p[1 + offset] << 8);
result |= (uint)(p[2 + offset] << 16);
result |= (uint)(p[3 + offset] << 24);
c = result;
return 4;
}
// timediff was a macro in original Kcp. let's inline it if possible.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int TimeDiff(uint later, uint earlier)
{
return (int)(later - earlier);
}
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LRM\LRM.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]

View file

@ -0,0 +1,265 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace Mirror.SimpleWeb
{
public interface IBufferOwner
{
void Return(ArrayBuffer buffer);
}
public sealed class ArrayBuffer : IDisposable
{
readonly IBufferOwner owner;
public readonly byte[] array;
/// <summary>
/// number of bytes writen to buffer
/// </summary>
internal int count;
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
public void SetReleasesRequired(int required)
{
releasesRequired = required;
}
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
/// <remarks>
/// This value is normally 0, but can be changed to require release to be called multiple times
/// </remarks>
int releasesRequired;
public ArrayBuffer(IBufferOwner owner, int size)
{
this.owner = owner;
array = new byte[size];
}
public void Release()
{
int newValue = Interlocked.Decrement(ref releasesRequired);
if (newValue <= 0)
{
count = 0;
owner.Return(this);
}
}
public void Dispose()
{
Release();
}
public void CopyTo(byte[] target, int offset)
{
if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
Buffer.BlockCopy(array, 0, target, offset, count);
}
public void CopyFrom(ArraySegment<byte> segment)
{
CopyFrom(segment.Array, segment.Offset, segment.Count);
}
public void CopyFrom(byte[] source, int offset, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Buffer.BlockCopy(source, offset, array, 0, length);
}
public void CopyFrom(IntPtr bufferPtr, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Marshal.Copy(bufferPtr, array, 0, length);
}
public ArraySegment<byte> ToSegment()
{
return new ArraySegment<byte>(array, 0, count);
}
[Conditional("UNITY_ASSERTIONS")]
internal void Validate(int arraySize)
{
if (array.Length != arraySize)
{
Log.Error("Buffer that was returned had an array of the wrong size");
}
}
}
internal class BufferBucket : IBufferOwner
{
public readonly int arraySize;
readonly ConcurrentQueue<ArrayBuffer> buffers;
/// <summary>
/// keeps track of how many arrays are taken vs returned
/// </summary>
internal int _current = 0;
public BufferBucket(int arraySize)
{
this.arraySize = arraySize;
buffers = new ConcurrentQueue<ArrayBuffer>();
}
public ArrayBuffer Take()
{
IncrementCreated();
if (buffers.TryDequeue(out ArrayBuffer buffer))
{
return buffer;
}
else
{
Log.Verbose($"BufferBucket({arraySize}) create new");
return new ArrayBuffer(this, arraySize);
}
}
public void Return(ArrayBuffer buffer)
{
DecrementCreated();
buffer.Validate(arraySize);
buffers.Enqueue(buffer);
}
[Conditional("DEBUG")]
void IncrementCreated()
{
int next = Interlocked.Increment(ref _current);
Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
[Conditional("DEBUG")]
void DecrementCreated()
{
int next = Interlocked.Decrement(ref _current);
Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
}
/// <summary>
/// Collection of different sized buffers
/// </summary>
/// <remarks>
/// <para>
/// Problem: <br/>
/// * Need to cached byte[] so that new ones aren't created each time <br/>
/// * Arrays sent are multiple different sizes <br/>
/// * Some message might be big so need buffers to cover that size <br/>
/// * Most messages will be small compared to max message size <br/>
/// </para>
/// <br/>
/// <para>
/// Solution: <br/>
/// * Create multiple groups of buffers covering the range of allowed sizes <br/>
/// * Split range exponentially (using math.log) so that there are more groups for small buffers <br/>
/// </para>
/// </remarks>
public class BufferPool
{
internal readonly BufferBucket[] buckets;
readonly int bucketCount;
readonly int smallest;
readonly int largest;
public BufferPool(int bucketCount, int smallest, int largest)
{
if (bucketCount < 2) throw new ArgumentException("Count must be at least 2");
if (smallest < 1) throw new ArgumentException("Smallest must be at least 1");
if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest");
this.bucketCount = bucketCount;
this.smallest = smallest;
this.largest = largest;
// split range over log scale (more buckets for smaller sizes)
double minLog = Math.Log(this.smallest);
double maxLog = Math.Log(this.largest);
double range = maxLog - minLog;
double each = range / (bucketCount - 1);
buckets = new BufferBucket[bucketCount];
for (int i = 0; i < bucketCount; i++)
{
double size = smallest * Math.Pow(Math.E, each * i);
buckets[i] = new BufferBucket((int)Math.Ceiling(size));
}
Validate();
// Example
// 5 count
// 20 smallest
// 16400 largest
// 3.0 log 20
// 9.7 log 16400
// 6.7 range 9.7 - 3
// 1.675 each 6.7 / (5-1)
// 20 e^ (3 + 1.675 * 0)
// 107 e^ (3 + 1.675 * 1)
// 572 e^ (3 + 1.675 * 2)
// 3056 e^ (3 + 1.675 * 3)
// 16,317 e^ (3 + 1.675 * 4)
// perceision wont be lose when using doubles
}
[Conditional("UNITY_ASSERTIONS")]
void Validate()
{
if (buckets[0].arraySize != smallest)
{
Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}");
}
int largestBucket = buckets[bucketCount - 1].arraySize;
// rounded using Ceiling, so allowed to be 1 more that largest
if (largestBucket != largest && largestBucket != largest + 1)
{
Log.Error($"BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}");
}
}
public ArrayBuffer Take(int size)
{
if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); }
for (int i = 0; i < bucketCount; i++)
{
if (size <= buckets[i].arraySize)
{
return buckets[i].Take();
}
}
throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})");
}
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.SimpleWeb
{
internal sealed class Connection : IDisposable
{
public const int IdNotSet = -1;
readonly object disposedLock = new object();
public TcpClient client;
public int connId = IdNotSet;
public Stream stream;
public Thread receiveThread;
public Thread sendThread;
public ManualResetEventSlim sendPending = new ManualResetEventSlim(false);
public ConcurrentQueue<ArrayBuffer> sendQueue = new ConcurrentQueue<ArrayBuffer>();
public Action<Connection> onDispose;
volatile bool hasDisposed;
public Connection(TcpClient client, Action<Connection> onDispose)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.onDispose = onDispose;
}
/// <summary>
/// disposes client and stops threads
/// </summary>
public void Dispose()
{
Log.Verbose($"Dispose {ToString()}");
// check hasDisposed first to stop ThreadInterruptedException on lock
if (hasDisposed) { return; }
Log.Info($"Connection Close: {ToString()}");
lock (disposedLock)
{
// check hasDisposed again inside lock to make sure no other object has called this
if (hasDisposed) { return; }
hasDisposed = true;
// stop threads first so they don't try to use disposed objects
receiveThread.Interrupt();
sendThread?.Interrupt();
try
{
// stream
stream?.Dispose();
stream = null;
client.Dispose();
client = null;
}
catch (Exception e)
{
Log.Exception(e);
}
sendPending.Dispose();
// release all buffers in send queue
while (sendQueue.TryDequeue(out ArrayBuffer buffer))
{
buffer.Release();
}
onDispose.Invoke(this);
}
}
public override string ToString()
{
System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint;
return $"[Conn:{connId}, endPoint:{endpoint}]";
}
}
}

View file

@ -0,0 +1,72 @@
using System.Text;
namespace Mirror.SimpleWeb
{
/// <summary>
/// Constant values that should never change
/// <para>
/// Some values are from https://tools.ietf.org/html/rfc6455
/// </para>
/// </summary>
internal static class Constants
{
/// <summary>
/// Header is at most 4 bytes
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderSize = 4;
/// <summary>
/// Smallest size of header
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderMinSize = 2;
/// <summary>
/// bytes for short length
/// </summary>
public const int ShortLength = 2;
/// <summary>
/// Message mask is always 4 bytes
/// </summary>
public const int MaskSize = 4;
/// <summary>
/// Max size of a message for length to be 1 byte long
/// <para>
/// payload length between 0-125
/// </para>
/// </summary>
public const int BytePayloadLength = 125;
/// <summary>
/// if payload length is 126 when next 2 bytes will be the length
/// </summary>
public const int UshortPayloadLength = 126;
/// <summary>
/// if payload length is 127 when next 8 bytes will be the length
/// </summary>
public const int UlongPayloadLength = 127;
/// <summary>
/// Guid used for WebSocket Protocol
/// </summary>
public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public static readonly int HandshakeGUIDLength = HandshakeGUID.Length;
public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID);
/// <summary>
/// Handshake messages will end with \r\n\r\n
/// </summary>
public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
}
}

View file

@ -0,0 +1,10 @@
namespace Mirror.SimpleWeb
{
public enum EventType
{
Connected,
Data,
Disconnected,
Error
}
}

View file

@ -0,0 +1,94 @@
using System;
using Conditional = System.Diagnostics.ConditionalAttribute;
namespace Mirror.SimpleWeb
{
public static class Log
{
// used for Conditional
const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED);
const string DEBUG = nameof(DEBUG);
public enum Levels
{
none = 0,
error = 1,
warn = 2,
info = 3,
verbose = 4,
}
public static Levels level = Levels.none;
public static string BufferToString(byte[] buffer, int offset = 0, int? length = null)
{
return BitConverter.ToString(buffer, offset, length ?? buffer.Length);
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, byte[] buffer, int offset, int length)
{
if (level < Levels.verbose)
return;
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, ArrayBuffer arrayBuffer)
{
if (level < Levels.verbose)
return;
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Verbose(string msg, bool showColor = true)
{
if (level < Levels.verbose)
return;
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Info(string msg, bool showColor = true)
{
if (level < Levels.info)
return;
}
/// <summary>
/// An expected Exception was caught, useful for debugging but not important
/// </summary>
/// <param name="msg"></param>
/// <param name="showColor"></param>
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void InfoException(Exception e)
{
if (level < Levels.info)
return;
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Warn(string msg, bool showColor = true)
{
if (level < Levels.warn)
return;
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Error(string msg, bool showColor = true)
{
if (level < Levels.error)
return;
}
public static void Exception(Exception e)
{
// always log Exceptions
Console.WriteLine("SWT Exception: " + e);
}
}
}

View file

@ -0,0 +1,49 @@
using System;
namespace Mirror.SimpleWeb
{
public struct Message
{
public readonly int connId;
public readonly EventType type;
public readonly ArrayBuffer data;
public readonly Exception exception;
public Message(EventType type) : this()
{
this.type = type;
}
public Message(ArrayBuffer data) : this()
{
type = EventType.Data;
this.data = data;
}
public Message(Exception exception) : this()
{
type = EventType.Error;
this.exception = exception;
}
public Message(int connId, EventType type) : this()
{
this.connId = connId;
this.type = type;
}
public Message(int connId, ArrayBuffer data) : this()
{
this.connId = connId;
type = EventType.Data;
this.data = data;
}
public Message(int connId, Exception exception) : this()
{
this.connId = connId;
type = EventType.Error;
this.exception = exception;
}
}
}

View file

@ -0,0 +1,140 @@
using System.IO;
using System.Runtime.CompilerServices;
namespace Mirror.SimpleWeb
{
public static class MessageProcessor
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte FirstLengthByte(byte[] buffer) => (byte)(buffer[1] & 0b0111_1111);
public static bool NeedToReadShortLength(byte[] buffer)
{
byte lenByte = FirstLengthByte(buffer);
return lenByte >= Constants.UshortPayloadLength;
}
public static int GetOpcode(byte[] buffer)
{
return buffer[0] & 0b0000_1111;
}
public static int GetPayloadLength(byte[] buffer)
{
byte lenByte = FirstLengthByte(buffer);
return GetMessageLength(buffer, 0, lenByte);
}
public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask)
{
bool finished = (buffer[0] & 0b1000_0000) != 0; // has full message been sent
bool hasMask = (buffer[1] & 0b1000_0000) != 0; // true from clients, false from server, "All messages from the client to the server have this bit set"
int opcode = buffer[0] & 0b0000_1111; // expecting 1 - text message
byte lenByte = FirstLengthByte(buffer);
ThrowIfNotFinished(finished);
ThrowIfMaskNotExpected(hasMask, expectMask);
ThrowIfBadOpCode(opcode);
int msglen = GetMessageLength(buffer, 0, lenByte);
ThrowIfLengthZero(msglen);
ThrowIfMsgLengthTooLong(msglen, maxLength);
}
public static void ToggleMask(byte[] src, int sourceOffset, int messageLength, byte[] maskBuffer, int maskOffset)
{
ToggleMask(src, sourceOffset, src, sourceOffset, messageLength, maskBuffer, maskOffset);
}
public static void ToggleMask(byte[] src, int sourceOffset, ArrayBuffer dst, int messageLength, byte[] maskBuffer, int maskOffset)
{
ToggleMask(src, sourceOffset, dst.array, 0, messageLength, maskBuffer, maskOffset);
dst.count = messageLength;
}
public static void ToggleMask(byte[] src, int srcOffset, byte[] dst, int dstOffset, int messageLength, byte[] maskBuffer, int maskOffset)
{
for (int i = 0; i < messageLength; i++)
{
byte maskByte = maskBuffer[maskOffset + i % Constants.MaskSize];
dst[dstOffset + i] = (byte)(src[srcOffset + i] ^ maskByte);
}
}
/// <exception cref="InvalidDataException"></exception>
static int GetMessageLength(byte[] buffer, int offset, byte lenByte)
{
if (lenByte == Constants.UshortPayloadLength)
{
// header is 4 bytes long
ushort value = 0;
value |= (ushort)(buffer[offset + 2] << 8);
value |= buffer[offset + 3];
return value;
}
else if (lenByte == Constants.UlongPayloadLength)
{
throw new InvalidDataException("Max length is longer than allowed in a single message");
}
else // is less than 126
{
// header is 2 bytes long
return lenByte;
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfNotFinished(bool finished)
{
if (!finished)
{
throw new InvalidDataException("Full message should have been sent, if the full message wasn't sent it wasn't sent from this trasnport");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask)
{
if (hasMask != expectMask)
{
throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfBadOpCode(int opcode)
{
// 2 = binary
// 8 = close
if (opcode != 2 && opcode != 8)
{
throw new InvalidDataException("Expected opcode to be binary or close");
}
}
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfLengthZero(int msglen)
{
if (msglen == 0)
{
throw new InvalidDataException("Message length was zero");
}
}
/// <summary>
/// need to check this so that data from previous buffer isn't used
/// </summary>
/// <exception cref="InvalidDataException"></exception>
static void ThrowIfMsgLengthTooLong(int msglen, int maxLength)
{
if (msglen > maxLength)
{
throw new InvalidDataException("Message length is greater than max length");
}
}
}
}

View file

@ -0,0 +1,132 @@
using System;
using System.IO;
using System.Runtime.Serialization;
namespace Mirror.SimpleWeb
{
public static class ReadHelper
{
/// <summary>
/// Reads exactly length from stream
/// </summary>
/// <returns>outOffset + length</returns>
/// <exception cref="ReadHelperException"></exception>
public static int Read(Stream stream, byte[] outBuffer, int outOffset, int length)
{
int received = 0;
try
{
while (received < length)
{
int read = stream.Read(outBuffer, outOffset + received, length - received);
if (read == 0)
{
throw new ReadHelperException("returned 0");
}
received += read;
}
}
catch (AggregateException ae)
{
// if interrupt is called we don't care about Exceptions
Utils.CheckForInterupt();
// rethrow
ae.Handle(e => false);
}
if (received != length)
{
throw new ReadHelperException("returned not equal to length");
}
return outOffset + received;
}
/// <summary>
/// Reads and returns results. This should never throw an exception
/// </summary>
public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int length)
{
try
{
Read(stream, outBuffer, outOffset, length);
return true;
}
catch (ReadHelperException)
{
return false;
}
catch (IOException)
{
return false;
}
catch (Exception e)
{
Log.Exception(e);
return false;
}
}
public static int? SafeReadTillMatch(Stream stream, byte[] outBuffer, int outOffset, int maxLength, byte[] endOfHeader)
{
try
{
int read = 0;
int endIndex = 0;
int endLength = endOfHeader.Length;
while (true)
{
int next = stream.ReadByte();
if (next == -1) // closed
return null;
if (read >= maxLength)
{
Log.Error("SafeReadTillMatch exceeded maxLength");
return null;
}
outBuffer[outOffset + read] = (byte)next;
read++;
// if n is match, check n+1 next
if (endOfHeader[endIndex] == next)
{
endIndex++;
// when all is match return with read length
if (endIndex >= endLength)
{
return read;
}
}
// if n not match reset to 0
else
{
endIndex = 0;
}
}
}
catch (IOException e)
{
Log.InfoException(e);
return null;
}
catch (Exception e)
{
Log.Exception(e);
return null;
}
}
}
[Serializable]
public class ReadHelperException : Exception
{
public ReadHelperException(string message) : base(message) {}
protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View file

@ -0,0 +1,194 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Mirror.SimpleWeb
{
internal static class ReceiveLoop
{
public struct Config
{
public readonly Connection conn;
public readonly int maxMessageSize;
public readonly bool expectMask;
public readonly ConcurrentQueue<Message> queue;
public readonly BufferPool bufferPool;
public Config(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool)
{
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
this.maxMessageSize = maxMessageSize;
this.expectMask = expectMask;
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
this.bufferPool = bufferPool ?? throw new ArgumentNullException(nameof(bufferPool));
}
public void Deconstruct(out Connection conn, out int maxMessageSize, out bool expectMask, out ConcurrentQueue<Message> queue, out BufferPool bufferPool)
{
conn = this.conn;
maxMessageSize = this.maxMessageSize;
expectMask = this.expectMask;
queue = this.queue;
bufferPool = this.bufferPool;
}
}
public static void Loop(Config config)
{
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool _) = config;
byte[] readBuffer = new byte[Constants.HeaderSize + (expectMask ? Constants.MaskSize : 0) + maxMessageSize];
try
{
try
{
TcpClient client = conn.client;
while (client.Connected)
{
ReadOneMessage(config, readBuffer);
}
Log.Info($"{conn} Not Connected");
}
catch (Exception)
{
// if interrupted we don't care about other exceptions
Utils.CheckForInterupt();
throw;
}
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (ObjectDisposedException e) { Log.InfoException(e); }
catch (ReadHelperException e)
{
// log as info only
Log.InfoException(e);
}
catch (SocketException e)
{
// this could happen if wss client closes stream
Log.Warn($"ReceiveLoop SocketException\n{e.Message}", false);
queue.Enqueue(new Message(conn.connId, e));
}
catch (IOException e)
{
// this could happen if client disconnects
Log.Warn($"ReceiveLoop IOException\n{e.Message}", false);
queue.Enqueue(new Message(conn.connId, e));
}
catch (InvalidDataException e)
{
Log.Error($"Invalid data from {conn}: {e.Message}");
queue.Enqueue(new Message(conn.connId, e));
}
catch (Exception e)
{
Log.Exception(e);
queue.Enqueue(new Message(conn.connId, e));
}
finally
{
conn.Dispose();
}
}
static void ReadOneMessage(Config config, byte[] buffer)
{
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
Stream stream = conn.stream;
int offset = 0;
// read 2
offset = ReadHelper.Read(stream, buffer, offset, Constants.HeaderMinSize);
// log after first blocking call
Log.Verbose($"Message From {conn}");
if (MessageProcessor.NeedToReadShortLength(buffer))
{
offset = ReadHelper.Read(stream, buffer, offset, Constants.ShortLength);
}
MessageProcessor.ValidateHeader(buffer, maxMessageSize, expectMask);
if (expectMask)
{
offset = ReadHelper.Read(stream, buffer, offset, Constants.MaskSize);
}
int opcode = MessageProcessor.GetOpcode(buffer);
int payloadLength = MessageProcessor.GetPayloadLength(buffer);
Log.Verbose($"Header ln:{payloadLength} op:{opcode} mask:{expectMask}");
Log.DumpBuffer($"Raw Header", buffer, 0, offset);
int msgOffset = offset;
offset = ReadHelper.Read(stream, buffer, offset, payloadLength);
switch (opcode)
{
case 2:
HandleArrayMessage(config, buffer, msgOffset, payloadLength);
break;
case 8:
HandleCloseMessage(config, buffer, msgOffset, payloadLength);
break;
}
}
static void HandleArrayMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
{
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
ArrayBuffer arrayBuffer = bufferPool.Take(payloadLength);
if (expectMask)
{
int maskOffset = msgOffset - Constants.MaskSize;
// write the result of toggle directly into arrayBuffer to avoid 2nd copy call
MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset);
}
else
{
arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength);
}
// dump after mask off
Log.DumpBuffer($"Message", arrayBuffer);
queue.Enqueue(new Message(conn.connId, arrayBuffer));
}
static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
{
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> _, BufferPool _) = config;
if (expectMask)
{
int maskOffset = msgOffset - Constants.MaskSize;
MessageProcessor.ToggleMask(buffer, msgOffset, payloadLength, buffer, maskOffset);
}
// dump after mask off
Log.DumpBuffer($"Message", buffer, msgOffset, payloadLength);
Log.Info($"Close: {GetCloseCode(buffer, msgOffset)} message:{GetCloseMessage(buffer, msgOffset, payloadLength)}");
conn.Dispose();
}
static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength)
{
return Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2);
}
static int GetCloseCode(byte[] buffer, int msgOffset)
{
return buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1];
}
}
}

View file

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
namespace Mirror.SimpleWeb
{
public static class SendLoopConfig
{
public static volatile bool batchSend = false;
public static volatile bool sleepBeforeSend = false;
}
internal static class SendLoop
{
public struct Config
{
public readonly Connection conn;
public readonly int bufferSize;
public readonly bool setMask;
public Config(Connection conn, int bufferSize, bool setMask)
{
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
this.bufferSize = bufferSize;
this.setMask = setMask;
}
public void Deconstruct(out Connection conn, out int bufferSize, out bool setMask)
{
conn = this.conn;
bufferSize = this.bufferSize;
setMask = this.setMask;
}
}
public static void Loop(Config config)
{
(Connection conn, int bufferSize, bool setMask) = config;
// create write buffer for this thread
byte[] writeBuffer = new byte[bufferSize];
MaskHelper maskHelper = setMask ? new MaskHelper() : null;
try
{
TcpClient client = conn.client;
Stream stream = conn.stream;
// null check in case disconnect while send thread is starting
if (client == null)
return;
while (client.Connected)
{
// wait for message
conn.sendPending.Wait();
// wait for 1ms for mirror to send other messages
if (SendLoopConfig.sleepBeforeSend)
{
Thread.Sleep(1);
}
conn.sendPending.Reset();
if (SendLoopConfig.batchSend)
{
int offset = 0;
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
{
// check if connected before sending message
if (!client.Connected) { Log.Info($"SendLoop {conn} not connected"); return; }
int maxLength = msg.count + Constants.HeaderSize + Constants.MaskSize;
// if next writer could overflow, write to stream and clear buffer
if (offset + maxLength > bufferSize)
{
stream.Write(writeBuffer, 0, offset);
offset = 0;
}
offset = SendMessage(writeBuffer, offset, msg, setMask, maskHelper);
msg.Release();
}
// after no message in queue, send remaining messages
// don't need to check offset > 0 because last message in queue will always be sent here
stream.Write(writeBuffer, 0, offset);
}
else
{
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
{
// check if connected before sending message
if (!client.Connected) { Log.Info($"SendLoop {conn} not connected"); return; }
int length = SendMessage(writeBuffer, 0, msg, setMask, maskHelper);
stream.Write(writeBuffer, 0, length);
msg.Release();
}
}
}
Log.Info($"{conn} Not Connected");
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e)
{
Log.Exception(e);
}
finally
{
conn.Dispose();
maskHelper?.Dispose();
}
}
/// <returns>new offset in buffer</returns>
static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool setMask, MaskHelper maskHelper)
{
int msgLength = msg.count;
int offset = WriteHeader(buffer, startOffset, msgLength, setMask);
if (setMask)
{
offset = maskHelper.WriteMask(buffer, offset);
}
msg.CopyTo(buffer, offset);
offset += msgLength;
// dump before mask on
Log.DumpBuffer("Send", buffer, startOffset, offset);
if (setMask)
{
int messageOffset = offset - msgLength;
MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize);
}
return offset;
}
static int WriteHeader(byte[] buffer, int startOffset, int msgLength, bool setMask)
{
int sendLength = 0;
const byte finished = 128;
const byte byteOpCode = 2;
buffer[startOffset + 0] = finished | byteOpCode;
sendLength++;
if (msgLength <= Constants.BytePayloadLength)
{
buffer[startOffset + 1] = (byte)msgLength;
sendLength++;
}
else if (msgLength <= ushort.MaxValue)
{
buffer[startOffset + 1] = 126;
buffer[startOffset + 2] = (byte)(msgLength >> 8);
buffer[startOffset + 3] = (byte)msgLength;
sendLength += 3;
}
else
{
throw new InvalidDataException($"Trying to send a message larger than {ushort.MaxValue} bytes");
}
if (setMask)
{
buffer[startOffset + 1] |= 0b1000_0000;
}
return sendLength + startOffset;
}
sealed class MaskHelper : IDisposable
{
readonly byte[] maskBuffer;
readonly RNGCryptoServiceProvider random;
public MaskHelper()
{
maskBuffer = new byte[4];
random = new RNGCryptoServiceProvider();
}
public void Dispose()
{
random.Dispose();
}
public int WriteMask(byte[] buffer, int offset)
{
random.GetBytes(maskBuffer);
Buffer.BlockCopy(maskBuffer, 0, buffer, offset, 4);
return offset + 4;
}
}
}
}

View file

@ -0,0 +1,25 @@
using System.Net.Sockets;
namespace Mirror.SimpleWeb
{
public struct TcpConfig
{
public readonly bool noDelay;
public readonly int sendTimeout;
public readonly int receiveTimeout;
public TcpConfig(bool noDelay, int sendTimeout, int receiveTimeout)
{
this.noDelay = noDelay;
this.sendTimeout = sendTimeout;
this.receiveTimeout = receiveTimeout;
}
public void ApplyTo(TcpClient client)
{
client.SendTimeout = sendTimeout;
client.ReceiveTimeout = receiveTimeout;
client.NoDelay = noDelay;
}
}
}

View file

@ -0,0 +1,13 @@
using System.Threading;
namespace Mirror.SimpleWeb
{
internal static class Utils
{
public static void CheckForInterupt()
{
// sleep in order to check for ThreadInterruptedException
Thread.Sleep(1);
}
}
}

View file

@ -0,0 +1,22 @@
SimpleWebTransport is a Transport that implements websocket for Webgl builds of
mirror. This transport can also work on standalone builds and has support for
encryption with websocket secure.
How to use:
Replace your existing Transport with SimpleWebTransport on your NetworkManager
Requirements:
Unity 2018.4 LTS
Mirror v18.0.0
Documentation:
https://mirror-networking.com/docs/
https://github.com/MirrorNetworking/SimpleWebTransport/blob/master/README.md
Support:
Discord: https://discordapp.com/invite/N9QVxbM
Bug Reports: https://github.com/MirrorNetworking/SimpleWebTransport/issues
**To get most recent updates and fixes download from github**
https://github.com/MirrorNetworking/SimpleWebTransport/releases

View file

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
namespace Mirror
{
class SWTConfig
{
public int maxMessageSize = 16 * 1024;
public int handshakeMaxSize = 3000;
public bool noDelay = true;
public int sendTimeout = 5000;
public int receiveTimeout = 20000;
public int serverMaxMessagesPerTick = 10000;
public bool waitBeforeSend = false;
public bool clientUseWss;
public bool sslEnabled;
public string sslCertJson = "./cert.json";
public SslProtocols sslProtocols = SslProtocols.Tls12;
}
}

View file

@ -0,0 +1,149 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Mirror.SimpleWeb
{
/// <summary>
/// Handles Handshakes from new clients on the server
/// <para>The server handshake has buffers to reduce allocations when clients connect</para>
/// </summary>
internal class ServerHandshake
{
const int GetSize = 3;
const int ResponseLength = 129;
const int KeyLength = 24;
const int MergedKeyLength = 60;
const string KeyHeaderString = "Sec-WebSocket-Key: ";
// this isn't an official max, just a reasonable size for a websocket handshake
readonly int maxHttpHeaderSize = 3000;
readonly SHA1 sha1 = SHA1.Create();
readonly BufferPool bufferPool;
public ServerHandshake(BufferPool bufferPool, int handshakeMaxSize)
{
this.bufferPool = bufferPool;
this.maxHttpHeaderSize = handshakeMaxSize;
}
~ServerHandshake()
{
sha1.Dispose();
}
public bool TryHandshake(Connection conn)
{
Stream stream = conn.stream;
using (ArrayBuffer getHeader = bufferPool.Take(GetSize))
{
if (!ReadHelper.TryRead(stream, getHeader.array, 0, GetSize))
return false;
getHeader.count = GetSize;
if (!IsGet(getHeader.array))
{
Log.Warn($"First bytes from client was not 'GET' for handshake, instead was {Log.BufferToString(getHeader.array, 0, GetSize)}");
return false;
}
}
string msg = ReadToEndForHandshake(stream);
if (string.IsNullOrEmpty(msg))
return false;
try
{
AcceptHandshake(stream, msg);
return true;
}
catch (ArgumentException e)
{
Log.InfoException(e);
return false;
}
}
string ReadToEndForHandshake(Stream stream)
{
using (ArrayBuffer readBuffer = bufferPool.Take(maxHttpHeaderSize))
{
int? readCountOrFail = ReadHelper.SafeReadTillMatch(stream, readBuffer.array, 0, maxHttpHeaderSize, Constants.endOfHandshake);
if (!readCountOrFail.HasValue)
return null;
int readCount = readCountOrFail.Value;
string msg = Encoding.ASCII.GetString(readBuffer.array, 0, readCount);
Log.Verbose(msg);
return msg;
}
}
static bool IsGet(byte[] getHeader)
{
// just check bytes here instead of using Encoding.ASCII
return getHeader[0] == 71 && // G
getHeader[1] == 69 && // E
getHeader[2] == 84; // T
}
void AcceptHandshake(Stream stream, string msg)
{
using (
ArrayBuffer keyBuffer = bufferPool.Take(KeyLength),
responseBuffer = bufferPool.Take(ResponseLength))
{
GetKey(msg, keyBuffer.array);
AppendGuid(keyBuffer.array);
byte[] keyHash = CreateHash(keyBuffer.array);
CreateResponse(keyHash, responseBuffer.array);
stream.Write(responseBuffer.array, 0, ResponseLength);
}
}
static void GetKey(string msg, byte[] keyBuffer)
{
int start = msg.IndexOf(KeyHeaderString) + KeyHeaderString.Length;
Log.Verbose($"Handshake Key: {msg.Substring(start, KeyLength)}");
Encoding.ASCII.GetBytes(msg, start, KeyLength, keyBuffer, 0);
}
static void AppendGuid(byte[] keyBuffer)
{
Buffer.BlockCopy(Constants.HandshakeGUIDBytes, 0, keyBuffer, KeyLength, Constants.HandshakeGUID.Length);
}
byte[] CreateHash(byte[] keyBuffer)
{
Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength)}");
return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength);
}
static void CreateResponse(byte[] keyHash, byte[] responseBuffer)
{
string keyHashString = Convert.ToBase64String(keyHash);
// compiler should merge these strings into 1 string before format
string message = string.Format(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: websocket\r\n" +
"Sec-WebSocket-Accept: {0}\r\n\r\n",
keyHashString);
Log.Verbose($"Handshake Response length {message.Length}, IsExpected {message.Length == ResponseLength}");
Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0);
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace Mirror.SimpleWeb
{
public struct SslConfig
{
public readonly bool enabled;
public readonly string certPath;
public readonly string certPassword;
public readonly SslProtocols sslProtocols;
public SslConfig(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols)
{
this.enabled = enabled;
this.certPath = certPath;
this.certPassword = certPassword;
this.sslProtocols = sslProtocols;
}
}
internal class ServerSslHelper
{
readonly SslConfig config;
readonly X509Certificate2 certificate;
public ServerSslHelper(SslConfig sslConfig)
{
config = sslConfig;
if (config.enabled)
certificate = new X509Certificate2(config.certPath, config.certPassword);
}
internal bool TryCreateStream(Connection conn)
{
NetworkStream stream = conn.client.GetStream();
if (config.enabled)
{
try
{
conn.stream = CreateStream(stream);
return true;
}
catch (Exception e)
{
Log.Error($"Create SSLStream Failed: {e}", false);
return false;
}
}
else
{
conn.stream = stream;
return true;
}
}
Stream CreateStream(NetworkStream stream)
{
SslStream sslStream = new SslStream(stream, true, acceptClient);
sslStream.AuthenticateAsServer(certificate, false, config.sslProtocols, false);
return sslStream;
}
bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// always accept client
return true;
}
}
}

View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
namespace Mirror.SimpleWeb
{
public class SimpleWebServer
{
readonly int maxMessagesPerTick;
readonly WebSocketServer server;
readonly BufferPool bufferPool;
public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig)
{
this.maxMessagesPerTick = maxMessagesPerTick;
// use max because bufferpool is used for both messages and handshake
int max = Math.Max(maxMessageSize, handshakeMaxSize);
bufferPool = new BufferPool(5, 20, max);
server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);
}
public bool Active { get; private set; }
public event Action<int> onConnect;
public event Action<int> onDisconnect;
public event Action<int, ArraySegment<byte>> onData;
public event Action<int, Exception> onError;
public void Start(ushort port)
{
server.Listen(port);
Active = true;
}
public void Stop()
{
server.Stop();
Active = false;
}
public void SendAll(List<int> connectionIds, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
buffer.SetReleasesRequired(connectionIds.Count);
// make copy of array before for each, data sent to each client is the same
foreach (int id in connectionIds)
{
server.Send(id, buffer);
}
}
public void SendOne(int connectionId, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
server.Send(connectionId, buffer);
}
public bool KickClient(int connectionId)
{
return server.CloseConnection(connectionId);
}
public string GetClientAddress(int connectionId)
{
return server.GetClientAddress(connectionId);
}
public void ProcessMessageQueue()
{
int processedCount = 0;
// check enabled every time in case behaviour was disabled after data
while (
processedCount < maxMessagesPerTick &&
// Dequeue last
server.receiveQueue.TryDequeue(out Message next)
)
{
processedCount++;
switch (next.type)
{
case EventType.Connected:
onConnect?.Invoke(next.connId);
break;
case EventType.Data:
onData?.Invoke(next.connId, next.data.ToSegment());
next.data.Release();
break;
case EventType.Disconnected:
onDisconnect?.Invoke(next.connId);
break;
case EventType.Error:
onError?.Invoke(next.connId, next.exception);
break;
}
}
}
}
}

View file

@ -0,0 +1,230 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.SimpleWeb
{
public class WebSocketServer
{
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
readonly TcpConfig tcpConfig;
readonly int maxMessageSize;
TcpListener listener;
Thread acceptThread;
bool serverStopped;
readonly ServerHandshake handShake;
readonly ServerSslHelper sslHelper;
readonly BufferPool bufferPool;
readonly ConcurrentDictionary<int, Connection> connections = new ConcurrentDictionary<int, Connection>();
int _idCounter = 0;
public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool)
{
this.tcpConfig = tcpConfig;
this.maxMessageSize = maxMessageSize;
sslHelper = new ServerSslHelper(sslConfig);
this.bufferPool = bufferPool;
handShake = new ServerHandshake(this.bufferPool, handshakeMaxSize);
}
public void Listen(int port)
{
listener = TcpListener.Create(port);
listener.Start();
Log.Info($"Server has started on port {port}");
acceptThread = new Thread(acceptLoop);
acceptThread.IsBackground = true;
acceptThread.Start();
}
public void Stop()
{
serverStopped = true;
// Interrupt then stop so that Exception is handled correctly
acceptThread?.Interrupt();
listener?.Stop();
acceptThread = null;
Log.Info("Server stopped, Closing all connections...");
// make copy so that foreach doesn't break if values are removed
Connection[] connectionsCopy = connections.Values.ToArray();
foreach (Connection conn in connectionsCopy)
{
conn.Dispose();
}
connections.Clear();
}
void acceptLoop()
{
try
{
try
{
while (true)
{
TcpClient client = listener.AcceptTcpClient();
tcpConfig.ApplyTo(client);
// TODO keep track of connections before they are in connections dictionary
// this might not be a problem as HandshakeAndReceiveLoop checks for stop
// and returns/disposes before sending message to queue
Connection conn = new Connection(client, AfterConnectionDisposed);
Log.Info($"A client connected {conn}");
// handshake needs its own thread as it needs to wait for message from client
Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn));
conn.receiveThread = receiveThread;
receiveThread.IsBackground = true;
receiveThread.Start();
}
}
catch (SocketException)
{
// check for Interrupted/Abort
Utils.CheckForInterupt();
throw;
}
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
}
void HandshakeAndReceiveLoop(Connection conn)
{
try
{
bool success = sslHelper.TryCreateStream(conn);
if (!success)
{
Log.Error($"Failed to create SSL Stream {conn}");
conn.Dispose();
return;
}
success = handShake.TryHandshake(conn);
if (success)
{
Log.Info($"Sent Handshake {conn}");
}
else
{
Log.Error($"Handshake Failed {conn}");
conn.Dispose();
return;
}
// check if Stop has been called since accepting this client
if (serverStopped)
{
Log.Info("Server stops after successful handshake");
return;
}
conn.connId = Interlocked.Increment(ref _idCounter);
connections.TryAdd(conn.connId, conn);
receiveQueue.Enqueue(new Message(conn.connId, EventType.Connected));
Thread sendThread = new Thread(() =>
{
SendLoop.Config sendConfig = new SendLoop.Config(
conn,
bufferSize: Constants.HeaderSize + maxMessageSize,
setMask: false);
SendLoop.Loop(sendConfig);
});
conn.sendThread = sendThread;
sendThread.IsBackground = true;
sendThread.Name = $"SendLoop {conn.connId}";
sendThread.Start();
ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config(
conn,
maxMessageSize,
expectMask: true,
receiveQueue,
bufferPool);
ReceiveLoop.Loop(receiveConfig);
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
finally
{
// close here in case connect fails
conn.Dispose();
}
}
void AfterConnectionDisposed(Connection conn)
{
if (conn.connId != Connection.IdNotSet)
{
receiveQueue.Enqueue(new Message(conn.connId, EventType.Disconnected));
connections.TryRemove(conn.connId, out Connection _);
}
}
public void Send(int id, ArrayBuffer buffer)
{
if (connections.TryGetValue(id, out Connection conn))
{
conn.sendQueue.Enqueue(buffer);
conn.sendPending.Set();
}
else
{
Log.Warn($"Cant send message to {id} because connection was not found in dictionary. Maybe it disconnected.");
}
}
public bool CloseConnection(int id)
{
if (connections.TryGetValue(id, out Connection conn))
{
Log.Info($"Kicking connection {id}");
conn.Dispose();
return true;
}
else
{
Log.Warn($"Failed to kick {id} because id not found");
return false;
}
}
public string GetClientAddress(int id)
{
if (connections.TryGetValue(id, out Connection conn))
{
return conn.client.Client.RemoteEndPoint.ToString();
}
else
{
Log.Error($"Cant close connection to {id} because connection was not found in dictionary");
return null;
}
}
}
}

View file

@ -0,0 +1,225 @@
using Mirror.SimpleWeb;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Security.Authentication;
namespace Mirror
{
public class SimpleWebTransport : Transport
{
public const string NormalScheme = "ws";
public const string SecureScheme = "wss";
public int maxMessageSize = 16 * 1024;
public int handshakeMaxSize = 3000;
public bool noDelay = true;
public int sendTimeout = 5000;
public int receiveTimeout = 20000;
public int serverMaxMessagesPerTick = 10000;
public int clientMaxMessagesPerTick = 1000;
public bool batchSend = true;
public bool waitBeforeSend = false;
public bool clientUseWss;
public bool sslEnabled;
public string sslCertJson = "./cert.json";
public SslProtocols sslProtocols = SslProtocols.Tls12;
Log.Levels _logLevels = Log.Levels.none;
/// <summary>
/// <para>Gets _logLevels field</para>
/// <para>Sets _logLevels and Log.level fields</para>
/// </summary>
public Log.Levels LogLevels
{
get => _logLevels;
set
{
_logLevels = value;
Log.level = _logLevels;
}
}
void OnValidate()
{
if (maxMessageSize > ushort.MaxValue)
{
Console.WriteLine($"max supported value for maxMessageSize is {ushort.MaxValue}");
maxMessageSize = ushort.MaxValue;
}
Log.level = _logLevels;
}
SimpleWebServer server;
TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout);
public override bool Available()
{
return true;
}
public override int GetMaxPacketSize(int channelId = 0)
{
return maxMessageSize;
}
void Awake()
{
Log.level = _logLevels;
SWTConfig conf = new SWTConfig();
if (!File.Exists("SWTConfig.json"))
{
File.WriteAllText("SWTConfig.json", JsonConvert.SerializeObject(conf, Formatting.Indented));
}
else
{
conf = JsonConvert.DeserializeObject<SWTConfig>(File.ReadAllText("SWTConfig.json"));
}
maxMessageSize = conf.maxMessageSize;
handshakeMaxSize = conf.handshakeMaxSize;
noDelay = conf.noDelay;
sendTimeout = conf.sendTimeout;
receiveTimeout = conf.receiveTimeout;
serverMaxMessagesPerTick = conf.serverMaxMessagesPerTick;
waitBeforeSend = conf.waitBeforeSend;
clientUseWss = conf.clientUseWss;
sslEnabled = conf.sslEnabled;
sslCertJson = conf.sslCertJson;
sslProtocols = conf.sslProtocols;
}
public override void Shutdown()
{
server?.Stop();
server = null;
}
#region Client
string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme;
string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;
public override bool ClientConnected()
{
// not null and not NotConnected (we want to return true if connecting or disconnecting)
return false;
}
public override void ClientConnect(string hostname) { }
public override void ClientDisconnect() { }
public override void ClientSend(int channelId, ArraySegment<byte> segment) { }
#endregion
#region Server
public override bool ServerActive()
{
return server != null && server.Active;
}
public override void ServerStart(ushort requestedPort)
{
if (ServerActive())
{
Console.WriteLine("SimpleWebServer Already Started");
}
SslConfig config = SslConfigLoader.Load(this);
server = new SimpleWebServer(serverMaxMessagesPerTick, TcpConfig, maxMessageSize, handshakeMaxSize, config);
server.onConnect += OnServerConnected.Invoke;
server.onDisconnect += OnServerDisconnected.Invoke;
server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, 0);
server.onError += OnServerError.Invoke;
SendLoopConfig.batchSend = batchSend || waitBeforeSend;
SendLoopConfig.sleepBeforeSend = waitBeforeSend;
server.Start(requestedPort);
}
public override void ServerStop()
{
if (!ServerActive())
{
Console.WriteLine("SimpleWebServer Not Active");
}
server.Stop();
server = null;
}
public override bool ServerDisconnect(int connectionId)
{
if (!ServerActive())
{
Console.WriteLine("SimpleWebServer Not Active");
return false;
}
return server.KickClient(connectionId);
}
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
if (!ServerActive())
{
Console.WriteLine("SimpleWebServer Not Active");
return;
}
if (segment.Count > maxMessageSize)
{
Console.WriteLine("Message greater than max size");
return;
}
if (segment.Count == 0)
{
Console.WriteLine("Message count was zero");
return;
}
server.SendOne(connectionId, segment);
return;
}
public override string ServerGetClientAddress(int connectionId)
{
return server.GetClientAddress(connectionId);
}
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder
{
Scheme = GetServerScheme(),
Host = Dns.GetHostName()
};
return builder.Uri;
}
public void Update()
{
server?.ProcessMessageQueue();
}
#endregion
}
}

View file

@ -0,0 +1,49 @@
using Newtonsoft.Json;
using System.IO;
namespace Mirror.SimpleWeb
{
internal class SslConfigLoader
{
internal struct Cert
{
public string path;
public string password;
}
internal static SslConfig Load(SimpleWebTransport transport)
{
// don't need to load anything if ssl is not enabled
if (!transport.sslEnabled)
return default;
string certJsonPath = transport.sslCertJson;
Cert cert = LoadCertJson(certJsonPath);
return new SslConfig(
enabled: transport.sslEnabled,
sslProtocols: transport.sslProtocols,
certPath: cert.path,
certPassword: cert.password
);
}
internal static Cert LoadCertJson(string certJsonPath)
{
string json = File.ReadAllText(certJsonPath);
Cert cert = JsonConvert.DeserializeObject<Cert>(json);
if (string.IsNullOrEmpty(cert.path))
{
throw new InvalidDataException("Cert Json didn't not contain \"path\"");
}
if (string.IsNullOrEmpty(cert.password))
{
// password can be empty
cert.password = string.Empty;
}
return cert;
}
}
}

View file

@ -0,0 +1,362 @@
using System;
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
// ClientState OBJECT that can be handed to the ReceiveThread safely.
// => allows us to create a NEW OBJECT every time we connect and start a
// receive thread.
// => perfectly protects us against data races. fixes all the flaky tests
// where .Connecting or .client would still be used by a dieing thread
// while attempting to use it for a new connection attempt etc.
// => creating a fresh client state each time is the best solution against
// data races here!
class ClientConnectionState : ConnectionState
{
public Thread receiveThread;
// TcpClient.Connected doesn't check if socket != null, which
// results in NullReferenceExceptions if connection was closed.
// -> let's check it manually instead
public bool Connected => client != null &&
client.Client != null &&
client.Client.Connected;
// TcpClient has no 'connecting' state to check. We need to keep track
// of it manually.
// -> checking 'thread.IsAlive && !Connected' is not enough because the
// thread is alive and connected is false for a short moment after
// disconnecting, so this would cause race conditions.
// -> we use a threadsafe bool wrapper so that ThreadFunction can remain
// static (it needs a common lock)
// => Connecting is true from first Connect() call in here, through the
// thread start, until TcpClient.Connect() returns. Simple and clear.
// => bools are atomic according to
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables
// made volatile so the compiler does not reorder access to it
public volatile bool Connecting;
// thread safe pipe for received messages
// => inside client connection state so that we can create a new state
// each time we connect
// (unlike server which has one receive pipe for all connections)
public readonly MagnificentReceivePipe receivePipe;
// constructor always creates new TcpClient for client connection!
public ClientConnectionState(int MaxMessageSize) : base(new TcpClient(), MaxMessageSize)
{
// create receive pipe with max message size for pooling
receivePipe = new MagnificentReceivePipe(MaxMessageSize);
}
// dispose all the state safely
public void Dispose()
{
// close client
client.Close();
// wait until thread finished. this is the only way to guarantee
// that we can call Connect() again immediately after Disconnect
// -> calling .Join would sometimes wait forever, e.g. when
// calling Disconnect while trying to connect to a dead end
receiveThread?.Interrupt();
// we interrupted the receive Thread, so we can't guarantee that
// connecting was reset. let's do it manually.
Connecting = false;
// clear send pipe. no need to hold on to elements.
// (unlike receiveQueue, which is still needed to process the
// latest Disconnected message, etc.)
sendPipe.Clear();
// IMPORTANT: DO NOT CLEAR RECEIVE PIPE.
// we still want to process disconnect messages in Tick()!
// let go of this client completely. the thread ended, no one uses
// it anymore and this way Connected is false again immediately.
client = null;
}
}
public class Client : Common
{
// events to hook into
// => OnData uses ArraySegment for allocation free receives later
public Action OnConnected;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// disconnect if send queue gets too big.
// -> avoids ever growing queue memory if network is slower than input
// -> disconnecting is great for load balancing. better to disconnect
// one connection than risking every connection / the whole server
// -> huge queue would introduce multiple seconds of latency anyway
//
// Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size:
// limit = 1,000 means 16 MB of memory/connection
// limit = 10,000 means 160 MB of memory/connection
public int SendQueueLimit = 10000;
public int ReceiveQueueLimit = 10000;
// all client state wrapped into an object that is passed to ReceiveThread
// => we create a new one each time we connect to avoid data races with
// old dieing threads still using the previous object!
ClientConnectionState state;
// Connected & Connecting
public bool Connected => state != null && state.Connected;
public bool Connecting => state != null && state.Connecting;
// pipe count, useful for debugging / benchmarks
public int ReceivePipeCount => state != null ? state.receivePipe.TotalCount : 0;
// constructor
public Client(int MaxMessageSize) : base(MaxMessageSize) {}
// the thread function
// STATIC to avoid sharing state!
// => pass ClientState object. a new one is created for each new thread!
// => avoids data races where an old dieing thread might still modify
// the current thread's state :/
static void ReceiveThreadFunction(ClientConnectionState state, string ip, int port, int MaxMessageSize, bool NoDelay, int SendTimeout, int ReceiveTimeout, int ReceiveQueueLimit)
{
Thread sendThread = null;
// absolutely must wrap with try/catch, otherwise thread
// exceptions are silent
try
{
// connect (blocking)
state.client.Connect(ip, port);
state.Connecting = false; // volatile!
// set socket options after the socket was created in Connect()
// (not after the constructor because we clear the socket there)
state.client.NoDelay = NoDelay;
state.client.SendTimeout = SendTimeout;
state.client.ReceiveTimeout = ReceiveTimeout;
// start send thread only after connected
// IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS!
sendThread = new Thread(() => { ThreadFunctions.SendLoop(0, state.client, state.sendPipe, state.sendPending); });
sendThread.IsBackground = true;
sendThread.Start();
// run the receive loop
// (receive pipe is shared across all loops)
ThreadFunctions.ReceiveLoop(0, state.client, MaxMessageSize, state.receivePipe, ReceiveQueueLimit);
}
catch (SocketException exception)
{
// this happens if (for example) the ip address is correct
// but there is no server running on that ip/port
Log.Info("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception);
// add 'Disconnected' event to receive pipe so that the caller
// knows that the Connect failed. otherwise they will never know
state.receivePipe.Enqueue(0, EventType.Disconnected, default);
}
catch (ThreadInterruptedException)
{
// expected if Disconnect() aborts it
}
catch (ThreadAbortException)
{
// expected if Disconnect() aborts it
}
catch (ObjectDisposedException)
{
// expected if Disconnect() aborts it and disposed the client
// while ReceiveThread is in a blocking Connect() call
}
catch (Exception exception)
{
// something went wrong. probably important.
Log.Error("Client Recv Exception: " + exception);
}
// sendthread might be waiting on ManualResetEvent,
// so let's make sure to end it if the connection
// closed.
// otherwise the send thread would only end if it's
// actually sending data while the connection is
// closed.
sendThread?.Interrupt();
// Connect might have failed. thread might have been closed.
// let's reset connecting state no matter what.
state.Connecting = false;
// if we got here then we are done. ReceiveLoop cleans up already,
// but we may never get there if connect fails. so let's clean up
// here too.
state.client?.Close();
}
public void Connect(string ip, int port)
{
// not if already started
if (Connecting || Connected)
{
Log.Warning("Telepathy Client can not create connection because an existing connection is connecting or connected");
return;
}
// overwrite old thread's state object. create a new one to avoid
// data races where an old dieing thread might still modify the
// current state! fixes all the flaky tests!
state = new ClientConnectionState(MaxMessageSize);
// We are connecting from now until Connect succeeds or fails
state.Connecting = true;
// create a TcpClient with perfect IPv4, IPv6 and hostname resolving
// support.
//
// * TcpClient(hostname, port): works but would connect (and block)
// already
// * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6
// addresses but only connects to IPv6 servers (e.g. Telepathy).
// does NOT connect to IPv4 servers (e.g. Mirror Booster), even
// with DualMode enabled.
// * TcpClient(): creates IPv4 socket internally, which would force
// Connect() to only use IPv4 sockets.
//
// => the trick is to clear the internal IPv4 socket so that Connect
// resolves the hostname and creates either an IPv4 or an IPv6
// socket as needed (see TcpClient source)
state.client.Client = null; // clear internal IPv4 socket until Connect()
// client.Connect(ip, port) is blocking. let's call it in the thread
// and return immediately.
// -> this way the application doesn't hang for 30s if connect takes
// too long, which is especially good in games
// -> this way we don't async client.BeginConnect, which seems to
// fail sometimes if we connect too many clients too fast
state.receiveThread = new Thread(() => {
ReceiveThreadFunction(state, ip, port, MaxMessageSize, NoDelay, SendTimeout, ReceiveTimeout, ReceiveQueueLimit);
});
state.receiveThread.IsBackground = true;
state.receiveThread.Start();
}
public void Disconnect()
{
// only if started
if (Connecting || Connected)
{
// dispose all the state safely
state.Dispose();
// IMPORTANT: DO NOT set state = null!
// we still want to process the pipe's disconnect message etc.!
}
}
// send message to server using socket connection.
// arraysegment for allocation free sends later.
// -> the segment's array is only used until Send() returns!
public bool Send(ArraySegment<byte> message)
{
if (Connected)
{
// respect max message size to avoid allocation attacks.
if (message.Count <= MaxMessageSize)
{
// check send pipe limit
if (state.sendPipe.Count < SendQueueLimit)
{
// add to thread safe send pipe and return immediately.
// calling Send here would be blocking (sometimes for long
// times if other side lags or wire was disconnected)
state.sendPipe.Enqueue(message);
state.sendPending.Set(); // interrupt SendThread WaitOne()
return true;
}
// disconnect if send queue gets too big.
// -> avoids ever growing queue memory if network is slower
// than input
// -> avoids ever growing latency as well
//
// note: while SendThread always grabs the WHOLE send queue
// immediately, it's still possible that the sending
// blocks for so long that the send queue just gets
// way too big. have a limit - better safe than sorry.
else
{
// log the reason
Log.Warning($"Client.Send: sendPipe reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting to avoid ever growing memory & latency.");
// just close it. send thread will take care of the rest.
state.client.Close();
return false;
}
}
Log.Error("Client.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize);
return false;
}
Log.Warning("Client.Send: not connected!");
return false;
}
// tick: processes up to 'limit' messages
// => limit parameter to avoid deadlocks / too long freezes if server or
// client is too slow to process network load
// => Mirror & DOTSNET need to have a process limit anyway.
// might as well do it here and make life easier.
// => returns amount of remaining messages to process, so the caller
// can call tick again as many times as needed (or up to a limit)
//
// Tick() may process multiple messages, but Mirror needs a way to stop
// processing immediately if a scene change messages arrives. Mirror
// can't process any other messages during a scene change.
// (could be useful for others too)
// => make sure to allocate the lambda only once in transports
public int Tick(int processLimit, Func<bool> checkEnabled = null)
{
// only if state was created yet (after connect())
// note: we don't check 'only if connected' because we want to still
// process Disconnect messages afterwards too!
if (state == null)
return 0;
// process up to 'processLimit' messages
for (int i = 0; i < processLimit; ++i)
{
// check enabled in case a Mirror scene message arrived
if (checkEnabled != null && !checkEnabled())
break;
// peek first. allows us to process the first queued entry while
// still keeping the pooled byte[] alive by not removing anything.
if (state.receivePipe.TryPeek(out int _, out EventType eventType, out ArraySegment<byte> message))
{
switch (eventType)
{
case EventType.Connected:
OnConnected?.Invoke();
break;
case EventType.Data:
OnData?.Invoke(message);
break;
case EventType.Disconnected:
OnDisconnected?.Invoke();
break;
}
// IMPORTANT: now dequeue and return it to pool AFTER we are
// done processing the event.
state.receivePipe.TryDequeue();
}
// no more messages. stop the loop.
else break;
}
// return what's left to process for next time
return state.receivePipe.TotalCount;
}
}
}

View file

@ -0,0 +1,39 @@
// common code used by server and client
namespace Telepathy
{
public abstract class Common
{
// IMPORTANT: DO NOT SHARE STATE ACROSS SEND/RECV LOOPS (DATA RACES)
// (except receive pipe which is used for all threads)
// NoDelay disables nagle algorithm. lowers CPU% and latency but
// increases bandwidth
public bool NoDelay = true;
// Prevent allocation attacks. Each packet is prefixed with a length
// header, so an attacker could send a fake packet with length=2GB,
// causing the server to allocate 2GB and run out of memory quickly.
// -> simply increase max packet size if you want to send around bigger
// files!
// -> 16KB per message should be more than enough.
public readonly int MaxMessageSize;
// Send would stall forever if the network is cut off during a send, so
// we need a timeout (in milliseconds)
public int SendTimeout = 5000;
// Default TCP receive time out can be huge (minutes).
// That's way too much for games, let's make it configurable.
// we need a timeout (in milliseconds)
// => '0' means disabled
// => disabled by default because some people might use Telepathy
// without Mirror and without sending pings, so timeouts are likely
public int ReceiveTimeout = 0;
// constructor
protected Common(int MaxMessageSize)
{
this.MaxMessageSize = MaxMessageSize;
}
}
}

View file

@ -0,0 +1,35 @@
// both server and client need a connection state object.
// -> server needs it to keep track of multiple connections
// -> client needs it to safely create a new connection state on every new
// connect in order to avoid data races where a dieing thread might still
// modify the current state. can't happen if we create a new state each time!
// (fixes all the flaky tests)
//
// ... besides, it also allows us to share code!
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
public class ConnectionState
{
public TcpClient client;
// thread safe pipe to send messages from main thread to send thread
public readonly MagnificentSendPipe sendPipe;
// ManualResetEvent to wake up the send thread. better than Thread.Sleep
// -> call Set() if everything was sent
// -> call Reset() if there is something to send again
// -> call WaitOne() to block until Reset was called
public ManualResetEvent sendPending = new ManualResetEvent(false);
public ConnectionState(TcpClient client, int MaxMessageSize)
{
this.client = client;
// create send pipe with max message size for pooling
sendPipe = new MagnificentSendPipe(MaxMessageSize);
}
}
}

View file

@ -0,0 +1,9 @@
namespace Telepathy
{
public enum EventType
{
Connected,
Data,
Disconnected
}
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018, vis2k
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,15 @@
// A simple logger class that uses Console.WriteLine by default.
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
// (this way we don't have to depend on UnityEngine.DLL and don't need a
// different version for every UnityEngine version here)
using System;
namespace Telepathy
{
public static class Log
{
public static Action<string> Info = Console.WriteLine;
public static Action<string> Warning = Console.WriteLine;
public static Action<string> Error = Console.Error.WriteLine;
}
}

View file

@ -0,0 +1 @@
// removed 2021-02-04

View file

@ -0,0 +1,222 @@
// a magnificent receive pipe to shield us from all of life's complexities.
// safely sends messages from receive thread to main thread.
// -> thread safety built in
// -> byte[] pooling coming in the future
//
// => hides all the complexity from telepathy
// => easy to switch between stack/queue/concurrentqueue/etc.
// => easy to test
using System;
using System.Collections.Generic;
namespace Telepathy
{
public class MagnificentReceivePipe
{
// queue entry message. only used in here.
// -> byte arrays are always of 4 + MaxMessageSize
// -> ArraySegment indicates the actual message content
struct Entry
{
public int connectionId;
public EventType eventType;
public ArraySegment<byte> data;
public Entry(int connectionId, EventType eventType, ArraySegment<byte> data)
{
this.connectionId = connectionId;
this.eventType = eventType;
this.data = data;
}
}
// message queue
// ConcurrentQueue allocates. lock{} instead.
//
// IMPORTANT: lock{} all usages!
readonly Queue<Entry> queue = new Queue<Entry>();
// byte[] pool to avoid allocations
// Take & Return is beautifully encapsulated in the pipe.
// the outside does not need to worry about anything.
// and it can be tested easily.
//
// IMPORTANT: lock{} all usages!
Pool<byte[]> pool;
// unfortunately having one receive pipe per connetionId is way slower
// in CCU tests. right now we have one pipe for all connections.
// => we still need to limit queued messages per connection to avoid one
// spamming connection being able to slow down everyone else since
// the queue would be full of just this connection's messages forever
// => let's use a simpler per-connectionId counter for now
Dictionary<int, int> queueCounter = new Dictionary<int, int>();
// constructor
public MagnificentReceivePipe(int MaxMessageSize)
{
// initialize pool to create max message sized byte[]s each time
pool = new Pool<byte[]>(() => new byte[MaxMessageSize]);
}
// return amount of queued messages for this connectionId.
// for statistics. don't call Count and assume that it's the same after
// the call.
public int Count(int connectionId)
{
lock (this)
{
return queueCounter.TryGetValue(connectionId, out int count)
? count
: 0;
}
}
// total count
public int TotalCount
{
get { lock (this) { return queue.Count; } }
}
// pool count for testing
public int PoolCount
{
get { lock (this) { return pool.Count(); } }
}
// enqueue a message
// -> ArraySegment to avoid allocations later
// -> parameters passed directly so it's more obvious that we don't just
// queue a passed 'Message', instead we copy the ArraySegment into
// a byte[] and store it internally, etc.)
public void Enqueue(int connectionId, EventType eventType, ArraySegment<byte> message)
{
// pool & queue usage always needs to be locked
lock (this)
{
// does this message have a data array content?
ArraySegment<byte> segment = default;
if (message != default)
{
// ArraySegment is only valid until returning.
// copy it into a byte[] that we can store.
// ArraySegment array is only valid until returning, so copy
// it into a byte[] that we can queue safely.
// get one from the pool first to avoid allocations
byte[] bytes = pool.Take();
// copy into it
Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count);
// indicate which part is the message
segment = new ArraySegment<byte>(bytes, 0, message.Count);
}
// enqueue it
// IMPORTANT: pass the segment around pool byte[],
// NOT the 'message' that is only valid until returning!
Entry entry = new Entry(connectionId, eventType, segment);
queue.Enqueue(entry);
// increase counter for this connectionId
int oldCount = Count(connectionId);
queueCounter[connectionId] = oldCount + 1;
}
}
// peek the next message
// -> allows the caller to process it while pipe still holds on to the
// byte[]
// -> TryDequeue should be called after processing, so that the message
// is actually dequeued and the byte[] is returned to pool!
// => see TryDequeue comments!
//
// IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD!
public bool TryPeek(out int connectionId, out EventType eventType, out ArraySegment<byte> data)
{
connectionId = 0;
eventType = EventType.Disconnected;
data = default;
// pool & queue usage always needs to be locked
lock (this)
{
if (queue.Count > 0)
{
Entry entry = queue.Peek();
connectionId = entry.connectionId;
eventType = entry.eventType;
data = entry.data;
return true;
}
return false;
}
}
// dequeue the next message
// -> simply dequeues and returns the byte[] to pool (if any)
// -> use Peek to actually process the first element while the pipe
// still holds on to the byte[]
// -> doesn't return the element because the byte[] needs to be returned
// to the pool in dequeue. caller can't be allowed to work with a
// byte[] that is already returned to pool.
// => Peek & Dequeue is the most simple, clean solution for receive
// pipe pooling to avoid allocations!
//
// IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD!
public bool TryDequeue()
{
// pool & queue usage always needs to be locked
lock (this)
{
if (queue.Count > 0)
{
// dequeue from queue
Entry entry = queue.Dequeue();
// return byte[] to pool (if any).
// not all message types have byte[] contents.
if (entry.data != default)
{
pool.Return(entry.data.Array);
}
// decrease counter for this connectionId
queueCounter[entry.connectionId]--;
// remove if zero. don't want to keep old connectionIds in
// there forever, it would cause slowly growing memory.
if (queueCounter[entry.connectionId] == 0)
queueCounter.Remove(entry.connectionId);
return true;
}
return false;
}
}
public void Clear()
{
// pool & queue usage always needs to be locked
lock (this)
{
// clear queue, but via dequeue to return each byte[] to pool
while (queue.Count > 0)
{
// dequeue
Entry entry = queue.Dequeue();
// return byte[] to pool (if any).
// not all message types have byte[] contents.
if (entry.data != default)
{
pool.Return(entry.data.Array);
}
}
// clear counter too
queueCounter.Clear();
}
}
}
}

View file

@ -0,0 +1,165 @@
// a magnificent send pipe to shield us from all of life's complexities.
// safely sends messages from main thread to send thread.
// -> thread safety built in
// -> byte[] pooling coming in the future
//
// => hides all the complexity from telepathy
// => easy to switch between stack/queue/concurrentqueue/etc.
// => easy to test
using System;
using System.Collections.Generic;
namespace Telepathy
{
public class MagnificentSendPipe
{
// message queue
// ConcurrentQueue allocates. lock{} instead.
// -> byte arrays are always of MaxMessageSize
// -> ArraySegment indicates the actual message content
//
// IMPORTANT: lock{} all usages!
readonly Queue<ArraySegment<byte>> queue = new Queue<ArraySegment<byte>>();
// byte[] pool to avoid allocations
// Take & Return is beautifully encapsulated in the pipe.
// the outside does not need to worry about anything.
// and it can be tested easily.
//
// IMPORTANT: lock{} all usages!
Pool<byte[]> pool;
// constructor
public MagnificentSendPipe(int MaxMessageSize)
{
// initialize pool to create max message sized byte[]s each time
pool = new Pool<byte[]>(() => new byte[MaxMessageSize]);
}
// for statistics. don't call Count and assume that it's the same after
// the call.
public int Count
{
get { lock (this) { return queue.Count; } }
}
// pool count for testing
public int PoolCount
{
get { lock (this) { return pool.Count(); } }
}
// enqueue a message
// arraysegment for allocation free sends later.
// -> the segment's array is only used until Enqueue() returns!
public void Enqueue(ArraySegment<byte> message)
{
// pool & queue usage always needs to be locked
lock (this)
{
// ArraySegment array is only valid until returning, so copy
// it into a byte[] that we can queue safely.
// get one from the pool first to avoid allocations
byte[] bytes = pool.Take();
// copy into it
Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count);
// indicate which part is the message
ArraySegment<byte> segment = new ArraySegment<byte>(bytes, 0, message.Count);
// now enqueue it
queue.Enqueue(segment);
}
}
// send threads need to dequeue each byte[] and write it into the socket
// -> dequeueing one byte[] after another works, but it's WAY slower
// than dequeueing all immediately (locks only once)
// lock{} & DequeueAll is WAY faster than ConcurrentQueue & dequeue
// one after another:
//
// uMMORPG 450 CCU
// SafeQueue: 900-1440ms latency
// ConcurrentQueue: 2000ms latency
//
// -> the most obvious solution is to just return a list with all byte[]
// (which allocates) and then write each one into the socket
// -> a faster solution is to serialize each one into one payload buffer
// and pass that to the socket only once. fewer socket calls always
// give WAY better CPU performance(!)
// -> to avoid allocating a new list of entries each time, we simply
// serialize all entries into the payload here already
// => having all this complexity built into the pipe makes testing and
// modifying the algorithm super easy!
//
// IMPORTANT: serializing in here will allow us to return the byte[]
// entries back to a pool later to completely avoid
// allocations!
public bool DequeueAndSerializeAll(ref byte[] payload, out int packetSize)
{
// pool & queue usage always needs to be locked
lock (this)
{
// do nothing if empty
packetSize = 0;
if (queue.Count == 0)
return false;
// we might have multiple pending messages. merge into one
// packet to avoid TCP overheads and improve performance.
//
// IMPORTANT: Mirror & DOTSNET already batch into MaxMessageSize
// chunks, but we STILL pack all pending messages
// into one large payload so we only give it to TCP
// ONCE. This is HUGE for performance so we keep it!
packetSize = 0;
foreach (ArraySegment<byte> message in queue)
packetSize += 4 + message.Count; // header + content
// create payload buffer if not created yet or previous one is
// too small
// IMPORTANT: payload.Length might be > packetSize! don't use it!
if (payload == null || payload.Length < packetSize)
payload = new byte[packetSize];
// dequeue all byte[] messages and serialize into the packet
int position = 0;
while (queue.Count > 0)
{
// dequeue
ArraySegment<byte> message = queue.Dequeue();
// write header (size) into buffer at position
Utils.IntToBytesBigEndianNonAlloc(message.Count, payload, position);
position += 4;
// copy message into payload at position
Buffer.BlockCopy(message.Array, message.Offset, payload, position, message.Count);
position += message.Count;
// return to pool so it can be reused (avoids allocations!)
pool.Return(message.Array);
}
// we did serialize something
return true;
}
}
public void Clear()
{
// pool & queue usage always needs to be locked
lock (this)
{
// clear queue, but via dequeue to return each byte[] to pool
while (queue.Count > 0)
{
pool.Return(queue.Dequeue().Array);
}
}
}
}
}

View file

@ -0,0 +1 @@
// removed 2021-02-04

View file

@ -0,0 +1,58 @@
using System.IO;
using System.Net.Sockets;
namespace Telepathy
{
public static class NetworkStreamExtensions
{
// .Read returns '0' if remote closed the connection but throws an
// IOException if we voluntarily closed our own connection.
//
// let's add a ReadSafely method that returns '0' in both cases so we don't
// have to worry about exceptions, since a disconnect is a disconnect...
public static int ReadSafely(this NetworkStream stream, byte[] buffer, int offset, int size)
{
try
{
return stream.Read(buffer, offset, size);
}
catch (IOException)
{
return 0;
}
}
// helper function to read EXACTLY 'n' bytes
// -> default .Read reads up to 'n' bytes. this function reads exactly
// 'n' bytes
// -> this is blocking until 'n' bytes were received
// -> immediately returns false in case of disconnects
public static bool ReadExactly(this NetworkStream stream, byte[] buffer, int amount)
{
// there might not be enough bytes in the TCP buffer for .Read to read
// the whole amount at once, so we need to keep trying until we have all
// the bytes (blocking)
//
// note: this just is a faster version of reading one after another:
// for (int i = 0; i < amount; ++i)
// if (stream.Read(buffer, i, 1) == 0)
// return false;
// return true;
int bytesRead = 0;
while (bytesRead < amount)
{
// read up to 'remaining' bytes with the 'safe' read extension
int remaining = amount - bytesRead;
int result = stream.ReadSafely(buffer, bytesRead, remaining);
// .Read returns 0 if disconnected
if (result == 0)
return false;
// otherwise add to bytes read
bytesRead += result;
}
return true;
}
}
}

View file

@ -0,0 +1,34 @@
// pool to avoid allocations. originally from libuv2k.
using System;
using System.Collections.Generic;
namespace Telepathy
{
public class Pool<T>
{
// objects
readonly Stack<T> objects = new Stack<T>();
// some types might need additional parameters in their constructor, so
// we use a Func<T> generator
readonly Func<T> objectGenerator;
// constructor
public Pool(Func<T> objectGenerator)
{
this.objectGenerator = objectGenerator;
}
// take an element from the pool, or create a new one if empty
public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator();
// return an element to the pool
public void Return(T item) => objects.Push(item);
// clear the pool with the disposer function applied to each object
public void Clear() => objects.Clear();
// count to see how many objects are in the pool. useful for tests.
public int Count() => objects.Count;
}
}

View file

@ -0,0 +1 @@
// removed 2021-02-04

View file

@ -0,0 +1,394 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
public class Server : Common
{
// events to hook into
// => OnData uses ArraySegment for allocation free receives later
public Action<int> OnConnected;
public Action<int, ArraySegment<byte>> OnData;
public Action<int> OnDisconnected;
// listener
public TcpListener listener;
Thread listenerThread;
// disconnect if send queue gets too big.
// -> avoids ever growing queue memory if network is slower than input
// -> disconnecting is great for load balancing. better to disconnect
// one connection than risking every connection / the whole server
// -> huge queue would introduce multiple seconds of latency anyway
//
// Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size:
// limit = 1,000 means 16 MB of memory/connection
// limit = 10,000 means 160 MB of memory/connection
public int SendQueueLimit = 10000;
public int ReceiveQueueLimit = 10000;
// thread safe pipe for received messages
// IMPORTANT: unfortunately using one pipe per connection is way slower
// when testing 150 CCU. we need to use one pipe for all
// connections. this scales beautifully.
protected MagnificentReceivePipe receivePipe;
// pipe count, useful for debugging / benchmarks
public int ReceivePipeTotalCount => receivePipe.TotalCount;
// clients with <connectionId, ConnectionState>
readonly ConcurrentDictionary<int, ConnectionState> clients = new ConcurrentDictionary<int, ConnectionState>();
// connectionId counter
int counter;
// public next id function in case someone needs to reserve an id
// (e.g. if hostMode should always have 0 connection and external
// connections should start at 1, etc.)
public int NextConnectionId()
{
int id = Interlocked.Increment(ref counter);
// it's very unlikely that we reach the uint limit of 2 billion.
// even with 1 new connection per second, this would take 68 years.
// -> but if it happens, then we should throw an exception because
// the caller probably should stop accepting clients.
// -> it's hardly worth using 'bool Next(out id)' for that case
// because it's just so unlikely.
if (id == int.MaxValue)
{
throw new Exception("connection id limit reached: " + id);
}
return id;
}
// check if the server is running
public bool Active => listenerThread != null && listenerThread.IsAlive;
// constructor
public Server(int MaxMessageSize) : base(MaxMessageSize) {}
// the listener thread's listen function
// note: no maxConnections parameter. high level API should handle that.
// (Transport can't send a 'too full' message anyway)
void Listen(int port)
{
// absolutely must wrap with try/catch, otherwise thread
// exceptions are silent
try
{
// start listener on all IPv4 and IPv6 address via .Create
listener = TcpListener.Create(port);
listener.Server.NoDelay = NoDelay;
listener.Server.SendTimeout = SendTimeout;
listener.Server.ReceiveTimeout = ReceiveTimeout;
listener.Start();
Log.Info("Server: listening port=" + port);
// keep accepting new clients
while (true)
{
// wait and accept new client
// note: 'using' sucks here because it will try to
// dispose after thread was started but we still need it
// in the thread
TcpClient client = listener.AcceptTcpClient();
// set socket options
client.NoDelay = NoDelay;
client.SendTimeout = SendTimeout;
client.ReceiveTimeout = ReceiveTimeout;
// generate the next connection id (thread safely)
int connectionId = NextConnectionId();
// add to dict immediately
ConnectionState connection = new ConnectionState(client, MaxMessageSize);
clients[connectionId] = connection;
// spawn a send thread for each client
Thread sendThread = new Thread(() =>
{
// wrap in try-catch, otherwise Thread exceptions
// are silent
try
{
// run the send loop
// IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS!
ThreadFunctions.SendLoop(connectionId, client, connection.sendPipe, connection.sendPending);
}
catch (ThreadAbortException)
{
// happens on stop. don't log anything.
// (we catch it in SendLoop too, but it still gets
// through to here when aborting. don't show an
// error.)
}
catch (Exception exception)
{
Log.Error("Server send thread exception: " + exception);
}
});
sendThread.IsBackground = true;
sendThread.Start();
// spawn a receive thread for each client
Thread receiveThread = new Thread(() =>
{
// wrap in try-catch, otherwise Thread exceptions
// are silent
try
{
// run the receive loop
// (receive pipe is shared across all loops)
ThreadFunctions.ReceiveLoop(connectionId, client, MaxMessageSize, receivePipe, ReceiveQueueLimit);
// IMPORTANT: do NOT remove from clients after the
// thread ends. need to do it in Tick() so that the
// disconnect event in the pipe is still processed.
// (removing client immediately would mean that the
// pipe is lost and the disconnect event is never
// processed)
// sendthread might be waiting on ManualResetEvent,
// so let's make sure to end it if the connection
// closed.
// otherwise the send thread would only end if it's
// actually sending data while the connection is
// closed.
sendThread.Interrupt();
}
catch (Exception exception)
{
Log.Error("Server client thread exception: " + exception);
}
});
receiveThread.IsBackground = true;
receiveThread.Start();
}
}
catch (ThreadAbortException exception)
{
// UnityEditor causes AbortException if thread is still
// running when we press Play again next time. that's okay.
Log.Info("Server thread aborted. That's okay. " + exception);
}
catch (SocketException exception)
{
// calling StopServer will interrupt this thread with a
// 'SocketException: interrupted'. that's okay.
Log.Info("Server Thread stopped. That's okay. " + exception);
}
catch (Exception exception)
{
// something went wrong. probably important.
Log.Error("Server Exception: " + exception);
}
}
// start listening for new connections in a background thread and spawn
// a new thread for each one.
public bool Start(int port)
{
// not if already started
if (Active) return false;
// create receive pipe with max message size for pooling
// => create new pipes every time!
// if an old receive thread is still finishing up, it might still
// be using the old pipes. we don't want to risk any old data for
// our new start here.
receivePipe = new MagnificentReceivePipe(MaxMessageSize);
// start the listener thread
// (on low priority. if main thread is too busy then there is not
// much value in accepting even more clients)
Log.Info("Server: Start port=" + port);
listenerThread = new Thread(() => { Listen(port); });
listenerThread.IsBackground = true;
listenerThread.Priority = ThreadPriority.BelowNormal;
listenerThread.Start();
return true;
}
public void Stop()
{
// only if started
if (!Active) return;
Log.Info("Server: stopping...");
// stop listening to connections so that no one can connect while we
// close the client connections
// (might be null if we call Stop so quickly after Start that the
// thread was interrupted before even creating the listener)
listener?.Stop();
// kill listener thread at all costs. only way to guarantee that
// .Active is immediately false after Stop.
// -> calling .Join would sometimes wait forever
listenerThread?.Interrupt();
listenerThread = null;
// close all client connections
foreach (KeyValuePair<int, ConnectionState> kvp in clients)
{
TcpClient client = kvp.Value.client;
// close the stream if not closed yet. it may have been closed
// by a disconnect already, so use try/catch
try { client.GetStream().Close(); } catch {}
client.Close();
}
// clear clients list
clients.Clear();
// reset the counter in case we start up again so
// clients get connection ID's starting from 1
counter = 0;
}
// send message to client using socket connection.
// arraysegment for allocation free sends later.
// -> the segment's array is only used until Send() returns!
public bool Send(int connectionId, ArraySegment<byte> message)
{
// respect max message size to avoid allocation attacks.
if (message.Count <= MaxMessageSize)
{
// find the connection
if (clients.TryGetValue(connectionId, out ConnectionState connection))
{
// check send pipe limit
if (connection.sendPipe.Count < SendQueueLimit)
{
// add to thread safe send pipe and return immediately.
// calling Send here would be blocking (sometimes for long
// times if other side lags or wire was disconnected)
connection.sendPipe.Enqueue(message);
connection.sendPending.Set(); // interrupt SendThread WaitOne()
return true;
}
// disconnect if send queue gets too big.
// -> avoids ever growing queue memory if network is slower
// than input
// -> disconnecting is great for load balancing. better to
// disconnect one connection than risking every
// connection / the whole server
//
// note: while SendThread always grabs the WHOLE send queue
// immediately, it's still possible that the sending
// blocks for so long that the send queue just gets
// way too big. have a limit - better safe than sorry.
else
{
// log the reason
Log.Warning($"Server.Send: sendPipe for connection {connectionId} reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting this connection for load balancing.");
// just close it. send thread will take care of the rest.
connection.client.Close();
return false;
}
}
// sending to an invalid connectionId is expected sometimes.
// for example, if a client disconnects, the server might still
// try to send for one frame before it calls GetNextMessages
// again and realizes that a disconnect happened.
// so let's not spam the console with log messages.
//Logger.Log("Server.Send: invalid connectionId: " + connectionId);
return false;
}
Log.Error("Server.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize);
return false;
}
// client's ip is sometimes needed by the server, e.g. for bans
public string GetClientAddress(int connectionId)
{
// find the connection
if (clients.TryGetValue(connectionId, out ConnectionState connection))
{
return ((IPEndPoint)connection.client.Client.RemoteEndPoint).Address.ToString();
}
return "";
}
// disconnect (kick) a client
public bool Disconnect(int connectionId)
{
// find the connection
if (clients.TryGetValue(connectionId, out ConnectionState connection))
{
// just close it. send thread will take care of the rest.
connection.client.Close();
Log.Info("Server.Disconnect connectionId:" + connectionId);
return true;
}
return false;
}
// tick: processes up to 'limit' messages for each connection
// => limit parameter to avoid deadlocks / too long freezes if server or
// client is too slow to process network load
// => Mirror & DOTSNET need to have a process limit anyway.
// might as well do it here and make life easier.
// => returns amount of remaining messages to process, so the caller
// can call tick again as many times as needed (or up to a limit)
//
// Tick() may process multiple messages, but Mirror needs a way to stop
// processing immediately if a scene change messages arrives. Mirror
// can't process any other messages during a scene change.
// (could be useful for others too)
// => make sure to allocate the lambda only once in transports
public int Tick(int processLimit, Func<bool> checkEnabled = null)
{
// only if pipe was created yet (after start())
if (receivePipe == null)
return 0;
// process up to 'processLimit' messages for this connection
for (int i = 0; i < processLimit; ++i)
{
// check enabled in case a Mirror scene message arrived
if (checkEnabled != null && !checkEnabled())
break;
// peek first. allows us to process the first queued entry while
// still keeping the pooled byte[] alive by not removing anything.
if (receivePipe.TryPeek(out int connectionId, out EventType eventType, out ArraySegment<byte> message))
{
switch (eventType)
{
case EventType.Connected:
OnConnected?.Invoke(connectionId);
break;
case EventType.Data:
OnData?.Invoke(connectionId, message);
break;
case EventType.Disconnected:
OnDisconnected?.Invoke(connectionId);
// remove disconnected connection now that the final
// disconnected message was processed.
clients.TryRemove(connectionId, out ConnectionState _);
break;
}
// IMPORTANT: now dequeue and return it to pool AFTER we are
// done processing the event.
receivePipe.TryDequeue();
}
// no more messages. stop the loop.
else break;
}
// return what's left to process for next time
return receivePipe.TotalCount;
}
}
}

View file

@ -0,0 +1 @@
// removed 2021-02-04

View file

@ -0,0 +1,244 @@
// IMPORTANT
// force all thread functions to be STATIC.
// => Common.Send/ReceiveLoop is EXTREMELY DANGEROUS because it's too easy to
// accidentally share Common state between threads.
// => header buffer, payload etc. were accidentally shared once after changing
// the thread functions from static to non static
// => C# does not automatically detect data races. best we can do is move all of
// our thread code into static functions and pass all state into them
//
// let's even keep them in a STATIC CLASS so it's 100% obvious that this should
// NOT EVER be changed to non static!
using System;
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
public static class ThreadFunctions
{
// send message (via stream) with the <size,content> message structure
// this function is blocking sometimes!
// (e.g. if someone has high latency or wire was cut off)
// -> payload is of multiple <<size, content, size, content, ...> parts
public static bool SendMessagesBlocking(NetworkStream stream, byte[] payload, int packetSize)
{
// stream.Write throws exceptions if client sends with high
// frequency and the server stops
try
{
// write the whole thing
stream.Write(payload, 0, packetSize);
return true;
}
catch (Exception exception)
{
// log as regular message because servers do shut down sometimes
Log.Info("Send: stream.Write exception: " + exception);
return false;
}
}
// read message (via stream) blocking.
// writes into byte[] and returns bytes written to avoid allocations.
public static bool ReadMessageBlocking(NetworkStream stream, int MaxMessageSize, byte[] headerBuffer, byte[] payloadBuffer, out int size)
{
size = 0;
// buffer needs to be of Header + MaxMessageSize
if (payloadBuffer.Length != 4 + MaxMessageSize)
{
Log.Error($"ReadMessageBlocking: payloadBuffer needs to be of size 4 + MaxMessageSize = {4 + MaxMessageSize} instead of {payloadBuffer.Length}");
return false;
}
// read exactly 4 bytes for header (blocking)
if (!stream.ReadExactly(headerBuffer, 4))
return false;
// convert to int
size = Utils.BytesToIntBigEndian(headerBuffer);
// protect against allocation attacks. an attacker might send
// multiple fake '2GB header' packets in a row, causing the server
// to allocate multiple 2GB byte arrays and run out of memory.
//
// also protect against size <= 0 which would cause issues
if (size > 0 && size <= MaxMessageSize)
{
// read exactly 'size' bytes for content (blocking)
return stream.ReadExactly(payloadBuffer, size);
}
Log.Warning("ReadMessageBlocking: possible header attack with a header of: " + size + " bytes.");
return false;
}
// thread receive function is the same for client and server's clients
public static void ReceiveLoop(int connectionId, TcpClient client, int MaxMessageSize, MagnificentReceivePipe receivePipe, int QueueLimit)
{
// get NetworkStream from client
NetworkStream stream = client.GetStream();
// every receive loop needs it's own receive buffer of
// HeaderSize + MaxMessageSize
// to avoid runtime allocations.
//
// IMPORTANT: DO NOT make this a member, otherwise every connection
// on the server would use the same buffer simulatenously
byte[] receiveBuffer = new byte[4 + MaxMessageSize];
// avoid header[4] allocations
//
// IMPORTANT: DO NOT make this a member, otherwise every connection
// on the server would use the same buffer simulatenously
byte[] headerBuffer = new byte[4];
// absolutely must wrap with try/catch, otherwise thread exceptions
// are silent
try
{
// add connected event to pipe
receivePipe.Enqueue(connectionId, EventType.Connected, default);
// let's talk about reading data.
// -> normally we would read as much as possible and then
// extract as many <size,content>,<size,content> messages
// as we received this time. this is really complicated
// and expensive to do though
// -> instead we use a trick:
// Read(2) -> size
// Read(size) -> content
// repeat
// Read is blocking, but it doesn't matter since the
// best thing to do until the full message arrives,
// is to wait.
// => this is the most elegant AND fast solution.
// + no resizing
// + no extra allocations, just one for the content
// + no crazy extraction logic
while (true)
{
// read the next message (blocking) or stop if stream closed
if (!ReadMessageBlocking(stream, MaxMessageSize, headerBuffer, receiveBuffer, out int size))
// break instead of return so stream close still happens!
break;
// create arraysegment for the read message
ArraySegment<byte> message = new ArraySegment<byte>(receiveBuffer, 0, size);
// send to main thread via pipe
// -> it'll copy the message internally so we can reuse the
// receive buffer for next read!
receivePipe.Enqueue(connectionId, EventType.Data, message);
// disconnect if receive pipe gets too big for this connectionId.
// -> avoids ever growing queue memory if network is slower
// than input
// -> disconnecting is great for load balancing. better to
// disconnect one connection than risking every
// connection / the whole server
if (receivePipe.Count(connectionId) >= QueueLimit)
{
// log the reason
Log.Warning($"receivePipe reached limit of {QueueLimit} for connectionId {connectionId}. This can happen if network messages come in way faster than we manage to process them. Disconnecting this connection for load balancing.");
// IMPORTANT: do NOT clear the whole queue. we use one
// queue for all connections.
//receivePipe.Clear();
// just break. the finally{} will close everything.
break;
}
}
}
catch (Exception exception)
{
// something went wrong. the thread was interrupted or the
// connection closed or we closed our own connection or ...
// -> either way we should stop gracefully
Log.Info("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception);
}
finally
{
// clean up no matter what
stream.Close();
client.Close();
// add 'Disconnected' message after disconnecting properly.
// -> always AFTER closing the streams to avoid a race condition
// where Disconnected -> Reconnect wouldn't work because
// Connected is still true for a short moment before the stream
// would be closed.
receivePipe.Enqueue(connectionId, EventType.Disconnected, default);
}
}
// thread send function
// note: we really do need one per connection, so that if one connection
// blocks, the rest will still continue to get sends
public static void SendLoop(int connectionId, TcpClient client, MagnificentSendPipe sendPipe, ManualResetEvent sendPending)
{
// get NetworkStream from client
NetworkStream stream = client.GetStream();
// avoid payload[packetSize] allocations. size increases dynamically as
// needed for batching.
//
// IMPORTANT: DO NOT make this a member, otherwise every connection
// on the server would use the same buffer simulatenously
byte[] payload = null;
try
{
while (client.Connected) // try this. client will get closed eventually.
{
// reset ManualResetEvent before we do anything else. this
// way there is no race condition. if Send() is called again
// while in here then it will be properly detected next time
// -> otherwise Send might be called right after dequeue but
// before .Reset, which would completely ignore it until
// the next Send call.
sendPending.Reset(); // WaitOne() blocks until .Set() again
// dequeue & serialize all
// a locked{} TryDequeueAll is twice as fast as
// ConcurrentQueue, see SafeQueue.cs!
if (sendPipe.DequeueAndSerializeAll(ref payload, out int packetSize))
{
// send messages (blocking) or stop if stream is closed
if (!SendMessagesBlocking(stream, payload, packetSize))
// break instead of return so stream close still happens!
break;
}
// don't choke up the CPU: wait until queue not empty anymore
sendPending.WaitOne();
}
}
catch (ThreadAbortException)
{
// happens on stop. don't log anything.
}
catch (ThreadInterruptedException)
{
// happens if receive thread interrupts send thread.
}
catch (Exception exception)
{
// something went wrong. the thread was interrupted or the
// connection closed or we closed our own connection or ...
// -> either way we should stop gracefully
Log.Info("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception);
}
finally
{
// clean up no matter what
// we might get SocketExceptions when sending if the 'host has
// failed to respond' - in which case we should close the connection
// which causes the ReceiveLoop to end and fire the Disconnected
// message. otherwise the connection would stay alive forever even
// though we can't send anymore.
stream.Close();
client.Close();
}
}
}
}

View file

@ -0,0 +1,23 @@
namespace Telepathy
{
public static class Utils
{
// IntToBytes version that doesn't allocate a new byte[4] each time.
// -> important for MMO scale networking performance.
public static void IntToBytesBigEndianNonAlloc(int value, byte[] bytes, int offset = 0)
{
bytes[offset + 0] = (byte)(value >> 24);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 3] = (byte)value;
}
public static int BytesToIntBigEndian(byte[] bytes)
{
return (bytes[0] << 24) |
(bytes[1] << 16) |
(bytes[2] << 8) |
bytes[3];
}
}
}

View file

@ -0,0 +1,58 @@
V1.7 [2021-02-20]
- ReceiveTimeout: disabled by default for cases where people use Telepathy by
itself without pings etc.
V1.6 [2021-02-10]
- configurable ReceiveTimeout to avoid TCPs high default timeout
- Server/Client receive queue limit now disconnects instead of showing a
warning. this is necessary for load balancing to avoid situations where one
spamming connection might fill the queue and slow down everyone else.
V1.5 [2021-02-05]
- fix: client data races & flaky tests fixed by creating a new client state
object every time we connect. fixes data race where an old dieing thread
might still try to modify the current state
- fix: Client.ReceiveThreadFunction catches and ignores ObjectDisposedException
which can happen if Disconnect() closes and disposes the client, while the
ReceiveThread just starts up and still uses the client.
- Server/Client Tick() optional enabled check for Mirror scene changing
V1.4 [2021-02-03]
- Server/Client.Tick: limit parameter added to process up to 'limit' messages.
makes Mirror & DOTSNET transports easier to implement
- stability: Server/Client send queue limit disconnects instead of showing a
warning. allows for load balancing. better to kick one connection and keep
the server running than slowing everything down for everyone.
V1.3 [2021-02-02]
- perf: ReceivePipe: byte[] pool for allocation free receives (╯°□°)╯︵ ┻━┻
- fix: header buffer, payload buffer data races because they were made non
static earlier. server threads would all access the same ones.
=> all threaded code was moved into a static ThreadFunctions class to make it
100% obvious that there should be no shared state in the future
V1.2 [2021-02-02]
- Client/Server Tick & OnConnected/OnData/OnDisconnected events instead of
having the outside process messages via GetNextMessage. That's easier for
Mirror/DOTSNET and allows for allocation free data message processing later.
- MagnificientSend/RecvPipe to shield Telepathy from all the complexity
- perf: SendPipe: byte[] pool for allocation free sends (╯°□°)╯︵ ┻━┻
V1.1 [2021-02-01]
- stability: added more tests
- breaking: Server/Client.Send: ArraySegment parameter and copy internally so
that Transports don't need to worry about it
- perf: Buffer.BlockCopy instead of Array.Copy
- perf: SendMessageBlocking puts message header directly into payload now
- perf: receiveQueues use SafeQueue instead of ConcurrentQueue to avoid
allocations
- Common: removed static state
- perf: SafeQueue.TryDequeueAll: avoid queue.ToArray() allocations. copy into a
list instead.
- Logger.Log/LogWarning/LogError renamed to Log.Info/Warning/Error
- MaxMessageSize is now specified in constructor to prepare for pooling
- flaky tests are ignored for now
- smaller improvements
V1.0
- first stable release

View file

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mirror
{
class TelepathyConfig
{
public bool NoDelay = true;
public int SendTimeout = 5000;
public int ReceiveTimeout = 30000;
public int serverMaxMessageSize = 16 * 1024;
public int serverMaxReceivesPerTick = 10000;
public int serverSendQueueLimitPerConnection = 10000;
public int serverReceiveQueueLimitPerConnection = 10000;
}
}

View file

@ -0,0 +1,195 @@
// wraps Telepathy for use as HLAPI TransportLayer
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
// Replaced by Kcp November 2020
namespace Mirror
{
public class TelepathyTransport : Transport
{
// scheme used by this transport
// "tcp4" means tcp with 4 bytes header, network byte order
public const string Scheme = "tcp4";
public bool NoDelay = true;
public int SendTimeout = 5000;
public int ReceiveTimeout = 30000;
public int serverMaxMessageSize = 16 * 1024;
public int serverMaxReceivesPerTick = 10000;
public int serverSendQueueLimitPerConnection = 10000;
public int serverReceiveQueueLimitPerConnection = 10000;
public int clientMaxMessageSize = 16 * 1024;
public int clientMaxReceivesPerTick = 1000;
public int clientSendQueueLimit = 10000;
public int clientReceiveQueueLimit = 10000;
Telepathy.Client client;
Telepathy.Server server;
// scene change message needs to halt message processing immediately
// Telepathy.Tick() has a enabledCheck parameter that we can use, but
// let's only allocate it once.
Func<bool> enabledCheck;
void Awake()
{
TelepathyConfig conf = new TelepathyConfig();
if (!File.Exists("TelepathyConfig.json"))
{
File.WriteAllText("TelepathyConfig.json", JsonConvert.SerializeObject(conf, Formatting.Indented));
}
else
{
conf = JsonConvert.DeserializeObject<TelepathyConfig>(File.ReadAllText("TelepathyConfig.json"));
}
NoDelay = conf.NoDelay;
SendTimeout = conf.SendTimeout;
ReceiveTimeout = conf.ReceiveTimeout;
serverMaxMessageSize = conf.serverMaxMessageSize;
serverMaxReceivesPerTick = conf.serverMaxReceivesPerTick;
serverSendQueueLimitPerConnection = conf.serverSendQueueLimitPerConnection;
serverReceiveQueueLimitPerConnection = conf.serverReceiveQueueLimitPerConnection;
// create client & server
client = new Telepathy.Client(clientMaxMessageSize);
server = new Telepathy.Server(serverMaxMessageSize);
// tell Telepathy to use Unity's Debug.Log
Telepathy.Log.Info = Console.WriteLine;
Telepathy.Log.Warning = Console.WriteLine;
Telepathy.Log.Error = Console.WriteLine;
// client hooks
// other systems hook into transport events in OnCreate or
// OnStartRunning in no particular order. the only way to avoid
// race conditions where telepathy uses OnConnected before another
// system's hook (e.g. statistics OnData) was added is to wrap
// them all in a lambda and always call the latest hook.
// (= lazy call)
client.OnConnected = () => OnClientConnected.Invoke();
client.OnData = (segment) => OnClientDataReceived.Invoke(segment, 0);
client.OnDisconnected = () => OnClientDisconnected.Invoke();
// client configuration
client.NoDelay = NoDelay;
client.SendTimeout = SendTimeout;
client.ReceiveTimeout = ReceiveTimeout;
client.SendQueueLimit = clientSendQueueLimit;
client.ReceiveQueueLimit = clientReceiveQueueLimit;
// server hooks
// other systems hook into transport events in OnCreate or
// OnStartRunning in no particular order. the only way to avoid
// race conditions where telepathy uses OnConnected before another
// system's hook (e.g. statistics OnData) was added is to wrap
// them all in a lambda and always call the latest hook.
// (= lazy call)
server.OnConnected = (connectionId) => OnServerConnected.Invoke(connectionId);
server.OnData = (connectionId, segment) => OnServerDataReceived.Invoke(connectionId, segment, 0);
server.OnDisconnected = (connectionId) => OnServerDisconnected.Invoke(connectionId);
// server configuration
server.NoDelay = NoDelay;
server.SendTimeout = SendTimeout;
server.ReceiveTimeout = ReceiveTimeout;
server.SendQueueLimit = serverSendQueueLimitPerConnection;
server.ReceiveQueueLimit = serverReceiveQueueLimitPerConnection;
// allocate enabled check only once
enabledCheck = () => true;
Console.WriteLine("TelepathyTransport initialized!");
}
public override bool Available()
{
// C#'s built in TCP sockets run everywhere except on WebGL
return true;
}
// client
public override bool ClientConnected() => client.Connected;
public override void ClientConnect(string address) { }
public override void ClientConnect(Uri uri) { }
public override void ClientSend(int channelId, ArraySegment<byte> segment) => client.Send(segment);
public override void ClientDisconnect() => client.Disconnect();
// messages should always be processed in early update
// server
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder();
builder.Scheme = Scheme;
builder.Host = Dns.GetHostName();
return builder.Uri;
}
public override bool ServerActive() => server.Active;
public override void ServerStart(ushort requestedPort) => server.Start(requestedPort);
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment) => server.Send(connectionId, segment);
public override bool ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
public override string ServerGetClientAddress(int connectionId)
{
try
{
return server.GetClientAddress(connectionId);
}
catch (SocketException)
{
// using server.listener.LocalEndpoint causes an Exception
// in UWP + Unity 2019:
// Exception thrown at 0x00007FF9755DA388 in UWF.exe:
// Microsoft C++ exception: Il2CppExceptionWrapper at memory
// location 0x000000E15A0FCDD0. SocketException: An address
// incompatible with the requested protocol was used at
// System.Net.Sockets.Socket.get_LocalEndPoint ()
// so let's at least catch it and recover
return "unknown";
}
}
public override void ServerStop() => server.Stop();
// messages should always be processed in early update
public void Update()
{
// note: we need to check enabled in case we set it to false
// when LateUpdate already started.
// (https://github.com/vis2k/Mirror/pull/379)
// process a maximum amount of server messages per tick
// IMPORTANT: check .enabled to stop processing immediately after a
// scene change message arrives!
server.Tick(serverMaxReceivesPerTick, enabledCheck);
}
// common
public override void Shutdown()
{
Console.WriteLine("TelepathyTransport Shutdown()");
client.Disconnect();
server.Stop();
}
public override int GetMaxPacketSize(int channelId)
{
return serverMaxMessageSize;
}
public override string ToString()
{
return "Telepathy";
}
}
}

71
UnityProject/.gitignore vendored Normal file
View file

@ -0,0 +1,71 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Asset meta data should only be ignored when the corresponding asset is also ignored
!/[Aa]ssets/**/*.meta
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Builds
*.apk
*.aab
*.unitypackage
# Crashlytics generated file
crashlytics-build.properties
# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*

6
UnityProject/.vsconfig Normal file
View file

@ -0,0 +1,6 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedGame"
]
}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 07be693ce1685a54c9bc5608bf627573
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 577d9725f58264943855b8ac185531fe
folderAsset: yes
timeCreated: 1466788344
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 14f21d7a1e53a8c4e87b25526a7eb63c
folderAsset: yes
timeCreated: 1466788345
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aadad8ac54f29e44583510294ac5c312
timeCreated: 1466788355
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,76 @@
fileFormatVersion: 2
guid: 6a3c684705042f345975d924f6983e36
timeCreated: 1466788352
licenseType: Store
PluginImporter:
serializedVersion: 1
iconMap: {}
executionOrder: {}
isPreloaded: 0
platformData:
Android:
enabled: 1
settings:
CPU: AnyCPU
Any:
enabled: 0
settings: {}
Editor:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
Linux:
enabled: 0
settings:
CPU: x86
Linux64:
enabled: 0
settings:
CPU: x86_64
OSXIntel:
enabled: 0
settings:
CPU: AnyCPU
OSXIntel64:
enabled: 0
settings:
CPU: AnyCPU
SamsungTV:
enabled: 1
settings:
STV_MODEL: STANDARD_13
Tizen:
enabled: 1
settings: {}
WebGL:
enabled: 1
settings: {}
Win:
enabled: 0
settings:
CPU: AnyCPU
Win64:
enabled: 0
settings:
CPU: AnyCPU
WindowsStoreApps:
enabled: 1
settings:
CPU: AnyCPU
DontProcess: False
PlaceholderPath: Assets/JsonDotNet/Assemblies/Standalone/Newtonsoft.Json.dll
SDK: AnySDK
ScriptingBackend: Il2Cpp
iOS:
enabled: 1
settings:
CompileFlags:
FrameworkDependencies:
tvOS:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 01ef782d02bb1994dbe418b69432552b
folderAsset: yes
timeCreated: 1466788344
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d6807fedb8dcaf04682d2c84f0ab753f
timeCreated: 1466788355
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,75 @@
fileFormatVersion: 2
guid: 17aef65a15b471f468b5fbeb4ff0c6a1
timeCreated: 1466788349
licenseType: Store
PluginImporter:
serializedVersion: 1
iconMap: {}
executionOrder: {}
isPreloaded: 0
platformData:
Android:
enabled: 0
settings:
CPU: AnyCPU
Any:
enabled: 0
settings: {}
Editor:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
Linux:
enabled: 1
settings:
CPU: x86
Linux64:
enabled: 1
settings:
CPU: x86_64
LinuxUniversal:
enabled: 1
settings:
CPU: AnyCPU
OSXIntel:
enabled: 1
settings:
CPU: AnyCPU
OSXIntel64:
enabled: 1
settings:
CPU: AnyCPU
OSXUniversal:
enabled: 1
settings:
CPU: AnyCPU
SamsungTV:
enabled: 0
settings:
STV_MODEL: STANDARD_13
Win:
enabled: 1
settings:
CPU: AnyCPU
Win64:
enabled: 1
settings:
CPU: AnyCPU
WindowsStoreApps:
enabled: 0
settings:
CPU: AnyCPU
DontProcess: False
PlaceholderPath:
SDK: AnySDK
ScriptingBackend: Il2Cpp
iOS:
enabled: 0
settings:
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 1418141139a6ac443b18cb05c0643a29
folderAsset: yes
timeCreated: 1466788345
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more