Merge branch 'main' of github.com:Derek-R-S/Light-Reflective-Mirror into docker
This commit is contained in:
commit
e952c45c28
1100 changed files with 114759 additions and 1807 deletions
BIN
LRM.png
Normal file
BIN
LRM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
README.md
32
README.md
|
|
@ -1,5 +1,26 @@
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Light Reflective Mirror
|
# Light Reflective Mirror
|
||||||
|
|
||||||
|
[](https://codeclimate.com/github/Derek-R-S/Light-Reflective-Mirror/maintainability)
|
||||||
|
|
||||||
|
LRM Node / MultiCompiled
|
||||||
|
|
||||||
|
[](http://monk3.xyz:90/project/AppVeyor/light-reflective-mirror/branch/main)
|
||||||
|
|
||||||
|
|
||||||
|
LoadBalancer
|
||||||
|
|
||||||
|
[](http://monk3.xyz:90/project/AppVeyor/light-reflective-mirror-canqw/branch/main)
|
||||||
|
|
||||||
|
Unity Package
|
||||||
|
|
||||||
|
[](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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
BIN
ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled.dll
Normal file
BIN
ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled.dll
Normal file
Binary file not shown.
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("kcp2k.Tests")]
|
||||||
1032
ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/KCP/kcp/Kcp.cs
Normal file
1032
ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/KCP/kcp/Kcp.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
|
||||||
|
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]
|
||||||
|
|
@ -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})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Mirror.SimpleWeb
|
||||||
|
{
|
||||||
|
public enum EventType
|
||||||
|
{
|
||||||
|
Connected,
|
||||||
|
Data,
|
||||||
|
Disconnected,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Telepathy
|
||||||
|
{
|
||||||
|
public enum EventType
|
||||||
|
{
|
||||||
|
Connected,
|
||||||
|
Data,
|
||||||
|
Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// removed 2021-02-04
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// removed 2021-02-04
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// removed 2021-02-04
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// removed 2021-02-04
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
71
UnityProject/.gitignore
vendored
Normal 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
6
UnityProject/.vsconfig
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"components": [
|
||||||
|
"Microsoft.VisualStudio.Workload.ManagedGame"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
UnityProject/Assets/JsonDotNet.meta
Normal file
8
UnityProject/Assets/JsonDotNet.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 07be693ce1685a54c9bc5608bf627573
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
9
UnityProject/Assets/JsonDotNet/Assemblies.meta
Normal file
9
UnityProject/Assets/JsonDotNet/Assemblies.meta
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 577d9725f58264943855b8ac185531fe
|
||||||
|
folderAsset: yes
|
||||||
|
timeCreated: 1466788344
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
9
UnityProject/Assets/JsonDotNet/Assemblies/AOT.meta
Normal file
9
UnityProject/Assets/JsonDotNet/Assemblies/AOT.meta
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 14f21d7a1e53a8c4e87b25526a7eb63c
|
||||||
|
folderAsset: yes
|
||||||
|
timeCreated: 1466788345
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8015
UnityProject/Assets/JsonDotNet/Assemblies/AOT/Newtonsoft.Json.XML
Normal file
8015
UnityProject/Assets/JsonDotNet/Assemblies/AOT/Newtonsoft.Json.XML
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aadad8ac54f29e44583510294ac5c312
|
||||||
|
timeCreated: 1466788355
|
||||||
|
licenseType: Store
|
||||||
|
TextScriptImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
|
|
@ -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:
|
||||||
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d6807fedb8dcaf04682d2c84f0ab753f
|
||||||
|
timeCreated: 1466788355
|
||||||
|
licenseType: Store
|
||||||
|
TextScriptImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
|
|
@ -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:
|
||||||
9
UnityProject/Assets/JsonDotNet/Assemblies/Windows.meta
Normal file
9
UnityProject/Assets/JsonDotNet/Assemblies/Windows.meta
Normal 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
Loading…
Reference in a new issue