Successful load balance test
This commit is contained in:
parent
6d72c947fe
commit
c162be240c
5 changed files with 186 additions and 64 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -12,25 +13,35 @@ using HttpStatusCode = Grapevine.HttpStatusCode;
|
||||||
|
|
||||||
namespace LightReflectiveMirror.LoadBalancing
|
namespace LightReflectiveMirror.LoadBalancing
|
||||||
{
|
{
|
||||||
|
|
||||||
[RestResource]
|
[RestResource]
|
||||||
public class Endpoint
|
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")]
|
[RestRoute("Get", "/api/auth")]
|
||||||
public async Task ReceiveAuthKey(IHttpContext context)
|
public async Task ReceiveAuthKey(IHttpContext context)
|
||||||
{
|
{
|
||||||
var req = context.Request;
|
var req = context.Request;
|
||||||
string receivedAuthKey = req.Headers["Auth"];
|
string receivedAuthKey = req.Headers["Auth"];
|
||||||
string port = req.Headers["Port"];
|
string endpointPort = req.Headers["EndpointPort"];
|
||||||
|
string gamePort = req.Headers["GamePort"];
|
||||||
|
|
||||||
string address = context.Request.RemoteEndPoint.Address.ToString();
|
string address = context.Request.RemoteEndPoint.Address.ToString();
|
||||||
|
|
||||||
Console.WriteLine("Received auth req [" + receivedAuthKey + "] == [" + Program.conf.AuthKey+"]");
|
Console.WriteLine("Received auth req [" + receivedAuthKey + "] == [" + Program.conf.AuthKey+"]");
|
||||||
|
|
||||||
// if server is authenticated
|
// if server is authenticated
|
||||||
if (receivedAuthKey != null && address != null && port != null && receivedAuthKey == Program.conf.AuthKey)
|
if (receivedAuthKey != null && address != null && endpointPort != null && gamePort != null && receivedAuthKey == Program.conf.AuthKey)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Server accepted: {address}:{port}");
|
Console.WriteLine($"Server accepted: {address}:{gamePort}");
|
||||||
|
var _gamePort = Convert.ToUInt16(gamePort);
|
||||||
await Program.instance.AddServer($"{address}:{port}");
|
var _endpointPort = Convert.ToUInt16(endpointPort);
|
||||||
|
await Program.instance.AddServer(address, _gamePort, _endpointPort);
|
||||||
|
|
||||||
await context.Response.SendResponseAsync(HttpStatusCode.Ok);
|
await context.Response.SendResponseAsync(HttpStatusCode.Ok);
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +68,7 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyValuePair<string, RelayStats> lowest = new("Dummy", new RelayStats { ConnectedClients = int.MaxValue });
|
KeyValuePair<RelayAddress, RelayStats> lowest = new(new RelayAddress { Address = "Dummy" }, new RelayStats { ConnectedClients = int.MaxValue });
|
||||||
|
|
||||||
for (int i = 0; i < servers.Count; i++)
|
for (int i = 0; i < servers.Count; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -69,7 +80,16 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
|
|
||||||
// respond with the server ip
|
// respond with the server ip
|
||||||
// if the string is still dummy then theres no servers
|
// if the string is still dummy then theres no servers
|
||||||
await context.Response.SendResponseAsync(lowest.Key != "Dummy" ? lowest.Key : HttpStatusCode.InternalServerError);
|
if (lowest.Key.Address != "Dummy")
|
||||||
|
{
|
||||||
|
// ping server to ensure its online.
|
||||||
|
await Program.instance.ManualPingServer(lowest.Key.Address, lowest.Key.Port);
|
||||||
|
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(lowest.Key));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.Response.SendResponseAsync(HttpStatusCode.InternalServerError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
/// Keeps track of all available relays.
|
/// Keeps track of all available relays.
|
||||||
/// Key is server address, value is CCU.
|
/// Key is server address, value is CCU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, RelayStats> availableRelayServers = new();
|
public Dictionary<RelayAddress, RelayStats> availableRelayServers = new();
|
||||||
|
|
||||||
private int _pingDelay = 10000;
|
private int _pingDelay = 10000;
|
||||||
const string API_PATH = "/api/stats";
|
const string API_PATH = "/api/stats";
|
||||||
|
|
@ -56,21 +56,21 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task AddServer(string serverIP)
|
public async Task AddServer(string serverIP, ushort port, ushort endpointPort)
|
||||||
{
|
{
|
||||||
var stats = await ManualPingServer(serverIP);
|
var stats = await ManualPingServer(serverIP, endpointPort);
|
||||||
|
|
||||||
if(stats.HasValue)
|
if(stats.HasValue)
|
||||||
availableRelayServers.Add(serverIP, stats.Value);
|
availableRelayServers.Add(new RelayAddress { Port = port, EndpointPort = endpointPort, Address = serverIP }, stats.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<RelayStats?> ManualPingServer(string serverIP)
|
public async Task<RelayStats?> ManualPingServer(string serverIP, ushort port)
|
||||||
{
|
{
|
||||||
using (WebClient wc = new WebClient())
|
using (WebClient wc = new WebClient())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}{API_PATH}");
|
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}");
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<RelayStats>(receivedStats);
|
return JsonConvert.DeserializeObject<RelayStats>(receivedStats);
|
||||||
}
|
}
|
||||||
|
|
@ -89,26 +89,25 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
WriteLogMessage("Pinging " + availableRelayServers.Count + " available relays");
|
WriteLogMessage("Pinging " + availableRelayServers.Count + " available relays");
|
||||||
|
|
||||||
// Create a new list so we can modify the collection in our loop.
|
// Create a new list so we can modify the collection in our loop.
|
||||||
var keys = new List<string>(availableRelayServers.Keys);
|
var keys = new List<RelayAddress>(availableRelayServers.Keys);
|
||||||
|
|
||||||
for(int i = 0; i < keys.Count; i++)
|
for(int i = 0; i < keys.Count; i++)
|
||||||
{
|
{
|
||||||
string url = $"http://{keys[i]}{API_PATH}";
|
string url = $"http://{keys[i].Address}:{keys[i].EndpointPort}{API_PATH}";
|
||||||
|
|
||||||
using (WebClient wc = new WebClient())
|
using (WebClient wc = new WebClient())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serverStats = wc.DownloadString(url);
|
var serverStats = wc.DownloadString(url);
|
||||||
Console.WriteLine(serverStats);
|
var deserializedData = JsonConvert.DeserializeObject<RelayStats>(serverStats);
|
||||||
|
|
||||||
WriteLogMessage("Server " + keys[i] + " still exists, keeping in collection.");
|
WriteLogMessage("Server " + keys[i].Address + " still exists, keeping in collection.");
|
||||||
|
|
||||||
if (availableRelayServers.ContainsKey(keys[i]))
|
if (availableRelayServers.ContainsKey(keys[i]))
|
||||||
availableRelayServers[keys[i]] = JsonConvert.DeserializeObject<RelayStats>(serverStats);
|
availableRelayServers[keys[i]] = deserializedData;
|
||||||
else
|
else
|
||||||
availableRelayServers.Add(keys[i], JsonConvert.DeserializeObject<RelayStats>(serverStats));
|
availableRelayServers.Add(keys[i], deserializedData);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -174,4 +173,13 @@ namespace LightReflectiveMirror.LoadBalancing
|
||||||
public int PublicRoomCount;
|
public int PublicRoomCount;
|
||||||
public TimeSpan Uptime;
|
public TimeSpan Uptime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct RelayAddress
|
||||||
|
{
|
||||||
|
public ushort Port;
|
||||||
|
public ushort EndpointPort;
|
||||||
|
public string Address;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,34 @@ namespace LightReflectiveMirror
|
||||||
{
|
{
|
||||||
class Config
|
class Config
|
||||||
{
|
{
|
||||||
|
//========================
|
||||||
|
// Required Settings
|
||||||
|
//========================
|
||||||
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 int UpdateLoopTime = 10;
|
public int UpdateLoopTime = 10;
|
||||||
public int UpdateHeartbeatInterval = 100;
|
public int UpdateHeartbeatInterval = 100;
|
||||||
|
|
||||||
|
//========================
|
||||||
|
// Endpoint REST API Settings
|
||||||
|
//========================
|
||||||
public bool UseEndpoint = true;
|
public bool UseEndpoint = true;
|
||||||
public ushort EndpointPort = 8080;
|
public ushort EndpointPort = 8080;
|
||||||
public bool EndpointServerList = true;
|
public bool EndpointServerList = true;
|
||||||
|
|
||||||
|
//========================
|
||||||
|
// Nat Puncher Settings
|
||||||
|
//========================
|
||||||
public bool EnableNATPunchtroughServer = true;
|
public bool EnableNATPunchtroughServer = true;
|
||||||
public ushort NATPunchtroughPort = 7776;
|
public ushort NATPunchtroughPort = 7776;
|
||||||
|
|
||||||
|
//========================
|
||||||
|
// Load Balancer Settings
|
||||||
|
//========================
|
||||||
|
public bool UseLoadBalancer = false;
|
||||||
|
public string LoadBalancerAuthKey = "AuthKey";
|
||||||
|
public string LoadBalancerAddress = "127.0.0.1";
|
||||||
|
public ushort LoadBalancerPort = 7070;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ namespace LightReflectiveMirror
|
||||||
private BiDictionary<int, string> _pendingNATPunches = new BiDictionary<int, string>();
|
private BiDictionary<int, string> _pendingNATPunches = new BiDictionary<int, string>();
|
||||||
private int _currentHeartbeatTimer = 0;
|
private int _currentHeartbeatTimer = 0;
|
||||||
|
|
||||||
private string _externalIp;
|
|
||||||
private byte[] _NATRequest = new byte[500];
|
private byte[] _NATRequest = new byte[500];
|
||||||
private int _NATRequestPosition = 0;
|
private int _NATRequestPosition = 0;
|
||||||
|
|
||||||
|
|
@ -63,7 +62,6 @@ namespace LightReflectiveMirror
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
|
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
|
||||||
_externalIp = await GetExternalIp();
|
|
||||||
|
|
||||||
WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true);
|
WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true);
|
||||||
try
|
try
|
||||||
|
|
@ -193,6 +191,7 @@ namespace LightReflectiveMirror
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(conf.UseLoadBalancer)
|
||||||
await RegisterSelfToLoadBalancer();
|
await RegisterSelfToLoadBalancer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,45 +216,33 @@ namespace LightReflectiveMirror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> RegisterSelfToLoadBalancer()
|
||||||
async Task<bool> RegisterSelfToLoadBalancer()
|
|
||||||
{
|
{
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// replace hard coded value for config value later
|
// replace hard coded value for config value later
|
||||||
var uri = new Uri("http://localhost:7070/api/auth");
|
var uri = new Uri($"http://{conf.LoadBalancerAddress}:{conf.LoadBalancerPort}/api/auth");
|
||||||
string externalip = _externalIp.Normalize().Trim();
|
string endpointPort = conf.EndpointPort.ToString();
|
||||||
string port = conf.EndpointPort.ToString();
|
string gamePort = 7777.ToString();
|
||||||
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(uri);
|
HttpWebRequest authReq = (HttpWebRequest)WebRequest.Create(uri);
|
||||||
|
|
||||||
myRequest.Headers.Add("Auth", "AuthKey");
|
authReq.Headers.Add("Auth", conf.LoadBalancerAuthKey);
|
||||||
myRequest.Headers.Add("Port", port);
|
authReq.Headers.Add("EndpointPort", endpointPort);
|
||||||
|
authReq.Headers.Add("GamePort", gamePort);
|
||||||
|
|
||||||
WebResponse myResponse = await myRequest.GetResponseAsync();
|
var res = await authReq.GetResponseAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// error adding or load balancer unavailable
|
// error adding or load balancer unavailable
|
||||||
WriteLogMessage("Error registering", ConsoleColor.Red);
|
WriteLogMessage("Error registering - Load Balancer probably timed out.", ConsoleColor.Red);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<string> GetExternalIp()
|
|
||||||
{
|
|
||||||
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create("https://ipv4.icanhazip.com/");
|
|
||||||
WebResponse myResponse = await myRequest.GetResponseAsync();
|
|
||||||
|
|
||||||
Stream stream = myResponse.GetResponseStream();
|
|
||||||
var ip = new StreamReader(stream).ReadToEnd();
|
|
||||||
|
|
||||||
return ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RunNATPunchLoop()
|
void RunNATPunchLoop()
|
||||||
{
|
{
|
||||||
WriteLogMessage("OK\n", ConsoleColor.Green);
|
WriteLogMessage("OK\n", ConsoleColor.Green);
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,27 @@ namespace LightReflectiveMirror
|
||||||
public bool connectOnAwake = true;
|
public bool connectOnAwake = true;
|
||||||
public string authenticationKey = "Secret Auth Key";
|
public string authenticationKey = "Secret Auth Key";
|
||||||
public UnityEvent diconnectedFromRelay;
|
public UnityEvent diconnectedFromRelay;
|
||||||
|
|
||||||
[Header("NAT Punchthrough")]
|
[Header("NAT Punchthrough")]
|
||||||
[Help("NAT Punchthrough will require the Direct Connect module attached.")]
|
[Help("NAT Punchthrough will require the Direct Connect module attached.")]
|
||||||
public bool useNATPunch = true;
|
public bool useNATPunch = true;
|
||||||
public ushort NATPunchtroughPort = 7776;
|
public ushort NATPunchtroughPort = 7776;
|
||||||
|
|
||||||
|
[Header("Load Balancer")]
|
||||||
|
public bool useLoadBalancer = false;
|
||||||
|
public ushort loadBalancerPort = 7070;
|
||||||
|
public string loadBalancerAddress = "127.0.0.1";
|
||||||
|
|
||||||
[Header("Server Hosting Data")]
|
[Header("Server Hosting Data")]
|
||||||
public string serverName = "My awesome server!";
|
public string serverName = "My awesome server!";
|
||||||
public string extraServerData = "Map 1";
|
public string extraServerData = "Map 1";
|
||||||
public int maxServerPlayers = 10;
|
public int maxServerPlayers = 10;
|
||||||
public bool isPublicServer = true;
|
public bool isPublicServer = true;
|
||||||
|
|
||||||
[Header("Server List")]
|
[Header("Server List")]
|
||||||
public UnityEvent serverListUpdated;
|
public UnityEvent serverListUpdated;
|
||||||
public List<RelayServerInfo> relayServerList { private set; get; } = new List<RelayServerInfo>();
|
public List<RelayServerInfo> relayServerList { private set; get; } = new List<RelayServerInfo>();
|
||||||
|
|
||||||
[Header("Server Information")]
|
[Header("Server Information")]
|
||||||
public int serverId = -1;
|
public int serverId = -1;
|
||||||
|
|
||||||
|
|
@ -198,16 +207,87 @@ namespace LightReflectiveMirror
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ConnectToRelay()
|
public void ConnectToRelay()
|
||||||
|
{
|
||||||
|
if (!useLoadBalancer)
|
||||||
{
|
{
|
||||||
if (!_connectedToRelay)
|
if (!_connectedToRelay)
|
||||||
{
|
{
|
||||||
_clientSendBuffer = new byte[clientToServerTransport.GetMaxPacketSize()];
|
Connect(serverIP);
|
||||||
|
|
||||||
clientToServerTransport.ClientConnect(serverIP);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.Log("Already connected to relay!");
|
Debug.LogWarning("LRM | Already connected to relay!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_connectedToRelay)
|
||||||
|
{
|
||||||
|
StartCoroutine(RelayConnect());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("LRM | Already connected to relay!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects to the desired relay
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverIP"></param>
|
||||||
|
private void Connect(string serverIP, ushort port = 7777)
|
||||||
|
{
|
||||||
|
// need to implement custom port
|
||||||
|
if (clientToServerTransport is LightReflectiveMirrorTransport)
|
||||||
|
throw new Exception("LRM | Client to Server Transport cannot be LRM.");
|
||||||
|
|
||||||
|
if (clientToServerTransport is kcp2k.KcpTransport kcp)
|
||||||
|
{
|
||||||
|
kcp.Port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clientSendBuffer = new byte[clientToServerTransport.GetMaxPacketSize()];
|
||||||
|
clientToServerTransport.ClientConnect(serverIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator RelayConnect()
|
||||||
|
{
|
||||||
|
string url = $"http://{loadBalancerAddress}:{loadBalancerPort}/api/join/";
|
||||||
|
|
||||||
|
using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
|
||||||
|
{
|
||||||
|
// Request and wait for the desired page.
|
||||||
|
yield return webRequest.SendWebRequest();
|
||||||
|
var result = webRequest.downloadHandler.text;
|
||||||
|
#if UNITY_2020_1_OR_NEWER
|
||||||
|
switch (webRequest.result)
|
||||||
|
{
|
||||||
|
case UnityWebRequest.Result.ConnectionError:
|
||||||
|
case UnityWebRequest.Result.DataProcessingError:
|
||||||
|
case UnityWebRequest.Result.ProtocolError:
|
||||||
|
Debug.LogWarning("LRM | Network Error while getting a relay to join from Load Balancer.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UnityWebRequest.Result.Success:
|
||||||
|
var parsedAddress = JsonConvert.DeserializeObject<RelayAddress>(result);
|
||||||
|
Connect(parsedAddress.Address, parsedAddress.Port);
|
||||||
|
endpointServerPort = parsedAddress.EndpointPort;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (webRequest.isNetworkError || webRequest.isHttpError)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("LRM | Network Error while getting a relay to join from Load Balancer.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// join here
|
||||||
|
var parsedAddress = JsonConvert.DeserializeObject<RelayAddress>(result);
|
||||||
|
Connect(parsedAddress.Address, parsedAddress.Port);
|
||||||
|
endpointServerPort = parsedAddress.EndpointPort;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +374,7 @@ namespace LightReflectiveMirror
|
||||||
int user = data.ReadInt(ref pos);
|
int user = data.ReadInt(ref pos);
|
||||||
if (_connectedRelayClients.TryGetByFirst(user, out int clientID))
|
if (_connectedRelayClients.TryGetByFirst(user, out int clientID))
|
||||||
{
|
{
|
||||||
OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(clientID));
|
OnServerDisconnected?.Invoke(clientID);
|
||||||
_connectedRelayClients.Remove(user);
|
_connectedRelayClients.Remove(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -749,4 +829,12 @@ namespace LightReflectiveMirror
|
||||||
public int serverId;
|
public int serverId;
|
||||||
public string serverData;
|
public string serverData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct RelayAddress
|
||||||
|
{
|
||||||
|
public ushort Port;
|
||||||
|
public ushort EndpointPort;
|
||||||
|
public string Address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue