Client-Side NAT punchtrough code

This commit is contained in:
Derek S 2021-04-03 21:52:21 -05:00
parent c5d2220b76
commit 2732aa90a1
5 changed files with 841 additions and 22 deletions

View file

@ -33,6 +33,11 @@ namespace LightReflectiveMirror
firstToSecond.Remove(first);
}
public ICollection<TFirst> GetAllKeys()
{
return secondToFirst.Values;
}
public bool TryGetBySecond(TSecond second, out TFirst first)
{
return secondToFirst.TryGetValue(second, out first);

View file

@ -0,0 +1,230 @@
// --------------------------------------------------------------------------------------------------------------------
/// <copyright file="HelpAttribute.cs">
/// <See cref="https://github.com/johnearnshaw/unity-inspector-help"></See>
/// Copyright (c) 2017, John Earnshaw, reblGreen Software Limited
/// <See cref="https://github.com/johnearnshaw/"></See>
/// <See cref="https://bitbucket.com/juanshaf/"></See>
/// <See cref="https://reblgreen.com/"></See>
/// All rights reserved.
/// Redistribution and use in source and binary forms, with or without modification, are
/// permitted provided that the following conditions are met:
/// 1. Redistributions of source code must retain the above copyright notice, this list of
/// conditions and the following disclaimer.
/// 2. Redistributions in binary form must reproduce the above copyright notice, this list
/// of conditions and the following disclaimer in the documentation and/or other materials
/// provided with the distribution.
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
/// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
/// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL THE
/// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
/// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
/// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
/// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
/// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
/// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[AttributeUsage(AttributeTargets.Field, Inherited = true)]
public class HelpAttribute : PropertyAttribute
{
public readonly string text;
// MessageType exists in UnityEditor namespace and can throw an exception when used outside the editor.
// We spoof MessageType at the bottom of this script to ensure that errors are not thrown when
// MessageType is unavailable.
public readonly MessageType type;
/// <summary>
/// Adds a HelpBox to the Unity property inspector above this field.
/// </summary>
/// <param name="text">The help text to be displayed in the HelpBox.</param>
/// <param name="type">The icon to be displayed in the HelpBox.</param>
public HelpAttribute(string text, MessageType type = MessageType.Info)
{
this.text = text;
this.type = type;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(HelpAttribute))]
public class HelpDrawer : PropertyDrawer
{
// Used for top and bottom padding between the text and the HelpBox border.
const int paddingHeight = 8;
// Used to add some margin between the the HelpBox and the property.
const int marginHeight = 2;
// Global field to store the original (base) property height.
float baseHeight = 0;
// Custom added height for drawing text area which has the MultilineAttribute.
float addedHeight = 0;
/// <summary>
/// A wrapper which returns the PropertyDrawer.attribute field as a HelpAttribute.
/// </summary>
HelpAttribute helpAttribute { get { return (HelpAttribute)attribute; } }
/// <summary>
/// A helper property to check for RangeAttribute.
/// </summary>
RangeAttribute rangeAttribute
{
get
{
var attributes = fieldInfo.GetCustomAttributes(typeof(RangeAttribute), true);
return attributes != null && attributes.Length > 0 ? (RangeAttribute)attributes[0] : null;
}
}
/// <summary>
/// A helper property to check for MultiLineAttribute.
/// </summary>
MultilineAttribute multilineAttribute
{
get
{
var attributes = fieldInfo.GetCustomAttributes(typeof(MultilineAttribute), true);
return attributes != null && attributes.Length > 0 ? (MultilineAttribute)attributes[0] : null;
}
}
public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
{
// We store the original property height for later use...
baseHeight = base.GetPropertyHeight(prop, label);
// This stops icon shrinking if text content doesn't fill out the container enough.
float minHeight = paddingHeight * 5;
// Calculate the height of the HelpBox using the GUIStyle on the current skin and the inspector
// window's currentViewWidth.
var content = new GUIContent(helpAttribute.text);
var style = GUI.skin.GetStyle("helpbox");
var height = style.CalcHeight(content, EditorGUIUtility.currentViewWidth);
// We add tiny padding here to make sure the text is not overflowing the HelpBox from the top
// and bottom.
height += marginHeight * 2;
// Since we draw a custom text area with the label above if our property contains the
// MultilineAttribute, we need to add some extra height to compensate. This is stored in a
// seperate global field so we can use it again later.
if (multilineAttribute != null && prop.propertyType == SerializedPropertyType.String)
{
addedHeight = 48f;
}
// If the calculated HelpBox is less than our minimum height we use this to calculate the returned
// height instead.
return height > minHeight ? height + baseHeight + addedHeight : minHeight + baseHeight + addedHeight;
}
public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label)
{
// We get a local reference to the MultilineAttribute as we use it twice in this method and it
// saves calling the logic twice for minimal optimization, etc...
var multiline = multilineAttribute;
EditorGUI.BeginProperty(position, label, prop);
// Copy the position out so we can calculate the position of our HelpBox without affecting the
// original position.
var helpPos = position;
helpPos.height -= baseHeight + marginHeight;
if (multiline != null)
{
helpPos.height -= addedHeight;
}
// Renders the HelpBox in the Unity inspector UI.
EditorGUI.HelpBox(helpPos, helpAttribute.text, helpAttribute.type);
position.y += helpPos.height + marginHeight;
position.height = baseHeight;
// If we have a RangeAttribute on our field, we need to handle the PropertyDrawer differently to
// keep the same style as Unity's default.
var range = rangeAttribute;
if (range != null)
{
if (prop.propertyType == SerializedPropertyType.Float)
{
EditorGUI.Slider(position, prop, range.min, range.max, label);
}
else if (prop.propertyType == SerializedPropertyType.Integer)
{
EditorGUI.IntSlider(position, prop, (int)range.min, (int)range.max, label);
}
else
{
// Not numeric so draw standard property field as punishment for adding RangeAttribute to
// a property which can not have a range :P
EditorGUI.PropertyField(position, prop, label);
}
}
else if (multiline != null)
{
// Here's where we handle the PropertyDrawer differently if we have a MultiLineAttribute, to try
// and keep some kind of multiline text area. This is not identical to Unity's default but is
// better than nothing...
if (prop.propertyType == SerializedPropertyType.String)
{
var style = GUI.skin.label;
var size = style.CalcHeight(label, EditorGUIUtility.currentViewWidth);
EditorGUI.LabelField(position, label);
position.y += size;
position.height += addedHeight - size;
// Fixed text dissappearing thanks to: http://answers.unity3d.com/questions/244043/textarea-does-not-work-text-dissapears-solution-is.html
prop.stringValue = EditorGUI.TextArea(position, prop.stringValue);
}
else
{
// Again with a MultilineAttribute on a non-text field deserves for the standard property field
// to be drawn as punishment :P
EditorGUI.PropertyField(position, prop, label);
}
}
else
{
// If we get to here it means we're drawing the default property field below the HelpBox. More custom
// and built in PropertyDrawers could be implemented to enable HelpBox but it could easily make for
// hefty else/if block which would need refactoring!
EditorGUI.PropertyField(position, prop, label);
}
EditorGUI.EndProperty();
}
}
#else
// Replicate MessageType Enum if we are not in editor as this enum exists in UnityEditor namespace.
// This should stop errors being logged the same as Shawn Featherly's commit in the Github repo but I
// feel is cleaner than having the conditional directive in the middle of the HelpAttribute constructor.
public enum MessageType
{
None,
Info,
Warning,
Error,
}
#endif

View file

@ -0,0 +1,159 @@
// This is an optional module for adding direct connect support
using Mirror;
using System;
using System.Collections.Generic;
using UnityEngine;
using LightReflectiveMirror;
[RequireComponent(typeof(LightReflectiveMirrorTransport))]
public class LRMDirectConnectModule : MonoBehaviour
{
public Transport directConnectTransport;
public bool showDebugLogs;
private LightReflectiveMirrorTransport lightMirrorTransport;
void Awake()
{
lightMirrorTransport = GetComponent<LightReflectiveMirrorTransport>();
if (directConnectTransport == null)
{
Debug.Log("Direct Connect Transport is null!");
return;
}
if (directConnectTransport is LightReflectiveMirrorTransport)
{
Debug.Log("Direct Connect Transport Cannot be the relay, silly. :P");
return;
}
directConnectTransport.OnServerConnected = (OnServerConnected);
directConnectTransport.OnServerDataReceived = (OnServerDataReceived);
directConnectTransport.OnServerDisconnected = (OnServerDisconnected);
directConnectTransport.OnServerError = (OnServerError);
directConnectTransport.OnClientConnected = (OnClientConnected);
directConnectTransport.OnClientDataReceived = (OnClientDataReceived);
directConnectTransport.OnClientDisconnected = (OnClientDisconnected);
directConnectTransport.OnClientError = (OnClientError);
}
public void StartServer(int port)
{
if(port > 0)
SetTransportPort(port);
directConnectTransport.ServerStart();
if (showDebugLogs)
Debug.Log("Direct Connect Server Created!");
}
public void StopServer()
{
directConnectTransport.ServerStop();
}
public void JoinServer(string ip, int port)
{
if(SupportsNATPunch())
SetTransportPort(port);
directConnectTransport.ClientConnect(ip);
}
public void SetTransportPort(int port)
{
if (directConnectTransport is kcp2k.KcpTransport kcpTransport)
kcpTransport.Port = (ushort)port;
else
{
throw new Exception("DIRECT CONNECT MODULE ONLY SUPPORTS KCP AT THE MOMENT.");
}
}
public int GetTransportPort()
{
if (directConnectTransport is kcp2k.KcpTransport kcpTransport)
return kcpTransport.Port;
else
{
throw new Exception("DIRECT CONNECT MODULE ONLY SUPPORTS KCP AT THE MOMENT.");
}
}
public bool SupportsNATPunch()
{
return directConnectTransport is kcp2k.KcpTransport;
}
public bool KickClient(int clientID)
{
if (showDebugLogs)
Debug.Log("Kicked direct connect client.");
return directConnectTransport.ServerDisconnect(clientID);
}
public void ClientDisconnect()
{
directConnectTransport.ClientDisconnect();
}
public void ServerSend(int clientID, ArraySegment<byte> data, int channel)
{
directConnectTransport.ServerSend(clientID, channel, data);
}
public void ClientSend(ArraySegment<byte> data, int channel)
{
directConnectTransport.ClientSend(channel, data);
}
#region Transport Callbacks
void OnServerConnected(int clientID)
{
if (showDebugLogs)
Debug.Log("Direct Connect Client Connected");
lightMirrorTransport.DirectAddClient(clientID);
}
void OnServerDataReceived(int clientID, ArraySegment<byte> data, int channel)
{
lightMirrorTransport.DirectReceiveData(data, channel, clientID);
}
void OnServerDisconnected(int clientID)
{
lightMirrorTransport.DirectRemoveClient(clientID);
}
void OnServerError(int client, Exception error)
{
if (showDebugLogs)
Debug.Log("Direct Server Error: " + error);
}
void OnClientConnected()
{
if (showDebugLogs)
Debug.Log("Direct Connect Client Joined");
lightMirrorTransport.DirectClientConnected();
}
void OnClientDisconnected()
{
lightMirrorTransport.DirectDisconnected();
}
void OnClientDataReceived(ArraySegment<byte> data, int channel)
{
lightMirrorTransport.DirectReceiveData(data, channel);
}
void OnClientError(Exception error)
{
if (showDebugLogs)
Debug.Log("Direct Client Error: " + error);
}
#endregion
}

View file

@ -4,6 +4,8 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
@ -21,6 +23,10 @@ namespace LightReflectiveMirror
public bool connectOnAwake = true;
public string authenticationKey = "Secret Auth Key";
public UnityEvent diconnectedFromRelay;
[Header("NAT Punchthrough")]
[Help("NAT Punchthrough will require the Direct Connect module attached.")]
public bool useNATPunch = true;
public ushort NATPunchtroughPort = 7776;
[Header("Server Hosting Data")]
public string serverName = "My awesome server!";
public string extraServerData = "Map 1";
@ -32,31 +38,137 @@ namespace LightReflectiveMirror
[Header("Server Information")]
public int serverId = -1;
private LRMDirectConnectModule _directConnectModule;
private byte[] _clientSendBuffer;
private bool _connectedToRelay = false;
private bool _isClient = false;
private bool _isServer = false;
private bool _directConnected = false;
private bool _isAuthenticated = false;
private int _currentMemberId;
private bool _callbacksInitialized = false;
private int _cachedHostID;
private BiDictionary<int, int> _connectedRelayClients = new BiDictionary<int, int>();
private BiDictionary<int, int> _connectedDirectClients = new BiDictionary<int, int>();
private UdpClient _NATPuncher;
private IPEndPoint _NATIP;
private IPEndPoint _relayPuncherIP;
private byte[] _punchData = new byte[1] { 1 };
private IPEndPoint _directConnectEndpoint;
private SocketProxy _clientProxy;
private BiDictionary<IPEndPoint, SocketProxy> _serverProxies = new BiDictionary<IPEndPoint, SocketProxy>();
public override bool ClientConnected() => _isClient;
private void OnConnectedToRelay() => _connectedToRelay = true;
public bool IsAuthenticated() => _isAuthenticated;
public override bool ServerActive() => _isServer;
public override bool Available() => _connectedToRelay;
public override void ClientEarlyUpdate() => clientToServerTransport.ClientEarlyUpdate();
public override void ClientLateUpdate() => clientToServerTransport.ClientLateUpdate();
public override void ClientConnect(Uri uri) => ClientConnect(uri.Host);
public override int GetMaxPacketSize(int channelId = 0) => clientToServerTransport.GetMaxPacketSize(channelId);
public override string ServerGetClientAddress(int connectionId) => _connectedRelayClients.GetBySecond(connectionId).ToString();
public override string ServerGetClientAddress(int connectionId) {
if (_connectedRelayClients.TryGetBySecond(connectionId, out int relayId))
return relayId.ToString();
if (_connectedDirectClients.TryGetBySecond(connectionId, out int directId))
return "DIRECT-" + directId;
// Shouldn't ever get here.
return "?";
}
public override void ClientEarlyUpdate()
{
clientToServerTransport.ClientEarlyUpdate();
if (_directConnectModule != null)
_directConnectModule.directConnectTransport.ClientEarlyUpdate();
}
public override void ClientLateUpdate()
{
clientToServerTransport.ClientLateUpdate();
if (_directConnectModule != null)
_directConnectModule.directConnectTransport.ClientLateUpdate();
}
public override void ServerEarlyUpdate()
{
if (_directConnectModule != null)
_directConnectModule.directConnectTransport.ServerEarlyUpdate();
}
void RecvData(IAsyncResult result)
{
IPEndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
var data = _NATPuncher.EndReceive(result, ref newClientEP);
if (!newClientEP.Address.Equals(_relayPuncherIP.Address))
{
if (_isServer)
{
if(_serverProxies.TryGetByFirst(newClientEP, out SocketProxy foundProxy))
{
if (data.Length > 2)
foundProxy.RelayData(data, data.Length);
}
else
{
_serverProxies.Add(newClientEP, new SocketProxy(_NATIP.Port + 1, newClientEP));
_serverProxies.GetByFirst(newClientEP).dataReceived += ServerProcessProxyData;
}
}
if (_isClient)
{
if(_clientProxy == null)
{
_clientProxy = new SocketProxy(_NATIP.Port - 1);
_clientProxy.dataReceived += ClientProcessProxyData;
}
else
{
_clientProxy.ClientRelayData(data, data.Length);
}
}
}
_NATPuncher.BeginReceive(new AsyncCallback(RecvData), _NATPuncher);
}
void ServerProcessProxyData(IPEndPoint remoteEndpoint, byte[] data)
{
_NATPuncher.Send(data, data.Length, remoteEndpoint);
}
void ClientProcessProxyData(IPEndPoint _, byte[] data)
{
_NATPuncher.Send(data, data.Length, _directConnectEndpoint);
}
public override void ServerLateUpdate()
{
if (_directConnectModule != null)
_directConnectModule.directConnectTransport.ServerLateUpdate();
}
private void Awake()
{
if (clientToServerTransport is LightReflectiveMirrorTransport)
throw new Exception("Haha real funny... Use a different transport.");
_directConnectModule = GetComponent<LRMDirectConnectModule>();
if(_directConnectModule != null)
{
if (useNATPunch && !_directConnectModule.SupportsNATPunch())
{
Debug.LogWarning("LRM | NATPunch is turned on but the transport used does not support it. It will be disabled.");
useNATPunch = false;
}
}
SetupCallbacks();
if (connectOnAwake)
@ -104,6 +216,20 @@ namespace LightReflectiveMirror
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, 200);
clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
if (_NATPuncher != null)
_NATPuncher.Send(new byte[] { 0 }, 1, _relayPuncherIP);
var keys = new List<IPEndPoint>(_serverProxies.GetAllKeys());
for (int i = 0; i < keys.Count; i++)
{
if (DateTime.Now.Subtract(_serverProxies.GetByFirst(keys[i]).lastInteractionTime).TotalSeconds > 10)
{
_serverProxies.GetByFirst(keys[i]).Dispose();
_serverProxies.Remove(keys[i]);
}
}
}
}
@ -115,6 +241,15 @@ namespace LightReflectiveMirror
Debug.Log("You must be connected to Relay to request server list!");
}
IEnumerator NATPunch(IPEndPoint remoteAddress)
{
for (int i = 0; i < 10; i++)
{
_NATPuncher.Send(_punchData, 1, remoteAddress);
yield return new WaitForSeconds(0.25f);
}
}
void DataReceived(ArraySegment<byte> segmentData, int channel)
{
try
@ -136,7 +271,10 @@ namespace LightReflectiveMirror
var recvData = data.ReadBytes(ref pos);
if (_isServer)
OnServerDataReceived?.Invoke(_connectedRelayClients.GetByFirst(data.ReadInt(ref pos)), new ArraySegment<byte>(recvData), channel);
{
if(_connectedRelayClients.TryGetByFirst(data.ReadInt(ref pos), out int clientID))
OnServerDataReceived?.Invoke(clientID, new ArraySegment<byte>(recvData), channel);
}
if (_isClient)
OnClientDataReceived?.Invoke(new ArraySegment<byte>(recvData), channel);
@ -152,8 +290,11 @@ namespace LightReflectiveMirror
if (_isServer)
{
int user = data.ReadInt(ref pos);
OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(user));
_connectedRelayClients.Remove(user);
if (_connectedRelayClients.TryGetByFirst(user, out int clientID))
{
OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(clientID));
_connectedRelayClients.Remove(user);
}
}
break;
case OpCodes.RoomCreated:
@ -172,14 +313,64 @@ namespace LightReflectiveMirror
_currentMemberId++;
}
break;
case OpCodes.DirectConnectIP:
var ip = data.ReadString(ref pos);
int port = data.ReadInt(ref pos);
bool attemptNatPunch = data.ReadBool(ref pos);
_directConnectEndpoint = new IPEndPoint(IPAddress.Parse(ip), port);
if (useNATPunch)
{
StartCoroutine(NATPunch(_directConnectEndpoint));
}
if (!_isServer)
{
if (_clientProxy == null && useNATPunch && attemptNatPunch)
{
_clientProxy = new SocketProxy(_NATIP.Port - 1);
_clientProxy.dataReceived += ClientProcessProxyData;
}
if (useNATPunch && attemptNatPunch)
_directConnectModule.JoinServer("127.0.0.1", _NATIP.Port - 1);
else
_directConnectModule.JoinServer(ip, port);
}
break;
case OpCodes.RequestNATConnection:
if (GetLocalIp() != null && _directConnectModule != null)
{
_NATPuncher = new UdpClient();
_NATPuncher.ExclusiveAddressUse = false;
_NATIP = new IPEndPoint(IPAddress.Parse(GetLocalIp()), UnityEngine.Random.Range(16000, 17000));
_NATPuncher.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_NATPuncher.Client.Bind(_NATIP);
_relayPuncherIP = new IPEndPoint(IPAddress.Parse(serverIP), NATPunchtroughPort);
byte[] initalData = new byte[150];
int sendPos = 0;
initalData.WriteBool(ref sendPos, true);
initalData.WriteString(ref sendPos, data.ReadString(ref pos));
// Send 3 to lower chance of it being dropped or corrupted when received on server.
_NATPuncher.Send(initalData, sendPos,_relayPuncherIP);
_NATPuncher.Send(initalData, sendPos,_relayPuncherIP);
_NATPuncher.Send(initalData, sendPos, _relayPuncherIP);
_NATPuncher.BeginReceive(new AsyncCallback(RecvData), _NATPuncher);
}
break;
}
}
catch { }
catch(Exception e) { print(e); }
}
IEnumerator GetServerList()
{
Uri uri = new Uri($"http://{serverIP}:{endpointServerPort}/api/servers");
string uri = $"http://{serverIP}:{endpointServerPort}/api/servers";
using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
{
@ -187,6 +378,7 @@ namespace LightReflectiveMirror
yield return webRequest.SendWebRequest();
var result = webRequest.downloadHandler.text;
#if UNITY_2020_1_OR_NEWER
switch (webRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
@ -209,6 +401,25 @@ namespace LightReflectiveMirror
break;
}
}
#else
if (webRequest.isNetworkError || webRequest.isHttpError)
{
Debug.LogWarning("LRM | Network Error while retreiving the server list!");
}
else
{
if (result == "Access Denied")
{
Debug.LogWarning("LRM | Server list request denied. Make sure you enable 'EndpointServerList' in server config!");
}
else
{
relayServerList?.Clear();
relayServerList = JsonConvert.DeserializeObject<List<RelayServerInfo>>(result);
serverListUpdated?.Invoke();
}
}
#endif
}
}
@ -266,8 +477,7 @@ namespace LightReflectiveMirror
public override void ClientConnect(string address)
{
int hostId = 0;
if (!Available() || !int.TryParse(address, out hostId))
if (!Available() || !int.TryParse(address, out _cachedHostID))
{
Debug.Log("Not connected to relay or invalid server id!");
OnClientDisconnected?.Invoke();
@ -278,8 +488,15 @@ namespace LightReflectiveMirror
throw new Exception("Cannot connect while hosting/already connected!");
int pos = 0;
_directConnected = false;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.JoinServer);
_clientSendBuffer.WriteInt(ref pos, hostId);
_clientSendBuffer.WriteInt(ref pos, _cachedHostID);
_clientSendBuffer.WriteBool(ref pos, _directConnectModule != null);
if (GetLocalIp() == null)
_clientSendBuffer.WriteString(ref pos, "0.0.0.0");
else
_clientSendBuffer.WriteString(ref pos, GetLocalIp());
_isClient = true;
@ -294,16 +511,26 @@ namespace LightReflectiveMirror
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom);
clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
if (_directConnectModule != null)
_directConnectModule.ClientDisconnect();
}
public override void ClientSend(int channelId, ArraySegment<byte> segment)
{
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData);
_clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray());
_clientSendBuffer.WriteInt(ref pos, 0);
if (_directConnected)
{
_directConnectModule.ClientSend(segment, channelId);
}
else
{
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData);
_clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray());
_clientSendBuffer.WriteInt(ref pos, 0);
clientToServerTransport.ClientSend(channelId, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
clientToServerTransport.ClientSend(channelId, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
}
}
public override bool ServerDisconnect(int connectionId)
@ -316,17 +543,27 @@ namespace LightReflectiveMirror
return true;
}
if(_connectedDirectClients.TryGetBySecond(connectionId, out int directId))
return _directConnectModule.KickClient(directId);
return false;
}
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData);
_clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray());
_clientSendBuffer.WriteInt(ref pos, _connectedRelayClients.GetBySecond(connectionId));
if (_directConnectModule != null && _connectedDirectClients.TryGetBySecond(connectionId, out int directId))
{
_directConnectModule.ServerSend(directId, segment, channelId);
}
else
{
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData);
_clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray());
_clientSendBuffer.WriteInt(ref pos, _connectedRelayClients.GetBySecond(connectionId));
clientToServerTransport.ClientSend(channelId, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
clientToServerTransport.ClientSend(channelId, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
}
}
public override void ServerStart()
@ -346,6 +583,15 @@ namespace LightReflectiveMirror
_isServer = true;
_connectedRelayClients = new BiDictionary<int, int>();
_currentMemberId = 1;
_connectedDirectClients = new BiDictionary<int, int>();
var keys = new List<IPEndPoint>(_serverProxies.GetAllKeys());
for(int i = 0; i < keys.Count; i++)
{
_serverProxies.GetByFirst(keys[i]).Dispose();
_serverProxies.Remove(keys[i]);
}
int pos = 0;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.CreateRoom);
@ -353,6 +599,28 @@ namespace LightReflectiveMirror
_clientSendBuffer.WriteString(ref pos, serverName);
_clientSendBuffer.WriteBool(ref pos, isPublicServer);
_clientSendBuffer.WriteString(ref pos, extraServerData);
// If we have direct connect module, and our local IP isnt null, tell server. Only time local IP is null is on cellular networks, such as IOS and Android.
_clientSendBuffer.WriteBool(ref pos, _directConnectModule != null ? GetLocalIp() != null ? true : false : false);
if (_directConnectModule != null && GetLocalIp() != null)
{
_clientSendBuffer.WriteString(ref pos, GetLocalIp());
// Transport port will be NAT port + 1 for the proxy connections.
_directConnectModule.StartServer(useNATPunch ? _NATIP.Port + 1 : -1);
}
else
_clientSendBuffer.WriteString(ref pos, "0.0.0.0");
if (useNATPunch)
{
_clientSendBuffer.WriteBool(ref pos, true);
_clientSendBuffer.WriteInt(ref pos, 0);
}
else
{
_clientSendBuffer.WriteBool(ref pos, false);
_clientSendBuffer.WriteInt(ref pos, _directConnectModule == null ? 1 : _directConnectModule.SupportsNATPunch() ? _directConnectModule.GetTransportPort() : 1);
}
clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
}
@ -366,6 +634,17 @@ namespace LightReflectiveMirror
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom);
clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
if (_directConnectModule != null)
_directConnectModule.StopServer();
var keys = new List<IPEndPoint>(_serverProxies.GetAllKeys());
for (int i = 0; i < keys.Count; i++)
{
_serverProxies.GetByFirst(keys[i]).Dispose();
_serverProxies.Remove(keys[i]);
}
}
}
@ -392,8 +671,87 @@ namespace LightReflectiveMirror
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, RequestServers = 15, ServerListReponse = 16, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19
LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20,
DirectConnectIP = 21
}
private static string GetLocalIp()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
return null;
}
#region Direct Connect Module
public void DirectAddClient(int clientID)
{
if (!_isServer)
return;
_connectedDirectClients.Add(clientID, _currentMemberId);
OnServerConnected?.Invoke(_currentMemberId);
_currentMemberId++;
}
public void DirectRemoveClient(int clientID)
{
if (!_isServer)
return;
OnServerDisconnected?.Invoke(_connectedDirectClients.GetByFirst(clientID));
_connectedDirectClients.Remove(clientID);
}
public void DirectReceiveData(ArraySegment<byte> data, int channel, int clientID = -1)
{
if (_isServer)
OnServerDataReceived?.Invoke(_connectedDirectClients.GetByFirst(clientID), data, channel);
if (_isClient)
OnClientDataReceived?.Invoke(data, channel);
}
public void DirectClientConnected()
{
_directConnected = true;
OnClientConnected?.Invoke();
}
public void DirectDisconnected()
{
if (_directConnected)
{
_isClient = false;
_directConnected = false;
OnClientDisconnected?.Invoke();
}
else
{
int pos = 0;
_directConnected = false;
_clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.JoinServer);
_clientSendBuffer.WriteInt(ref pos, _cachedHostID);
_clientSendBuffer.WriteBool(ref pos, false); // Direct failed, use relay
_isClient = true;
clientToServerTransport.ClientSend(0, new System.ArraySegment<byte>(_clientSendBuffer, 0, pos));
}
if (_clientProxy != null)
{
_clientProxy.Dispose();
_clientProxy = null;
}
}
#endregion
}
[Serializable]

View file

@ -0,0 +1,67 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
namespace LightReflectiveMirror
{
// This class handles the proxying from punched socket to transport.
public class SocketProxy
{
public DateTime lastInteractionTime;
public Action<IPEndPoint, byte[]> dataReceived;
UdpClient _udpClient;
IPEndPoint _recvEndpoint = new IPEndPoint(IPAddress.Any, 0);
IPEndPoint _remoteEndpoint;
bool _clientInitialRecv = false;
public SocketProxy(int port, IPEndPoint remoteEndpoint)
{
_udpClient = new UdpClient();
_udpClient.Connect(new IPEndPoint(IPAddress.Loopback, port));
_udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient);
lastInteractionTime = DateTime.Now;
// Clone it so when main socket recvies new data, it wont switcheroo on us.
_remoteEndpoint = new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port);
}
public SocketProxy(int port)
{
_udpClient = new UdpClient(port);
_udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient);
lastInteractionTime = DateTime.Now;
}
public void RelayData(byte[] data, int length)
{
_udpClient.Send(data, length);
lastInteractionTime = DateTime.Now;
}
public void ClientRelayData(byte[] data, int length)
{
if (_clientInitialRecv)
{
_udpClient.Send(data, length, _recvEndpoint);
lastInteractionTime = DateTime.Now;
}
}
public void Dispose()
{
_udpClient.Dispose();
}
void RecvData(IAsyncResult result)
{
_clientInitialRecv = true;
byte[] data = _udpClient.EndReceive(result, ref _recvEndpoint);
lastInteractionTime = DateTime.Now;
dataReceived?.Invoke(_remoteEndpoint, data);
_udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient);
}
}
}