diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientLayer.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientLayer.cs new file mode 100644 index 0000000..d1f49f2 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientLayer.cs @@ -0,0 +1,70 @@ +using LiteNetLib.Utils; +using System; +using System.Net; + +namespace FishNet.Transporting.Edgegap.Client +{ + public class EdgegapClientLayer : LiteNetLib.Layers.PacketLayerBase + { + private uint _userAuthorizationToken; + private uint _sessionAuthorizationToken; + private RelayConnectionState _prevState = RelayConnectionState.Disconnected; + + public Action OnStateChange; + + public EdgegapClientLayer(uint userAuthorizationToken, uint sessionAuthorizationToken) + : base(EdgegapProtocol.ClientOverhead) + { + _userAuthorizationToken = userAuthorizationToken; + _sessionAuthorizationToken = sessionAuthorizationToken; + } + + public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + NetDataReader reader = new NetDataReader(data, offset, length); + + var messageType = (MessageType)reader.GetByte(); + if (messageType == MessageType.Ping) + { + var currentState = (RelayConnectionState)reader.GetByte(); + + if (currentState != _prevState && OnStateChange != null) + { + OnStateChange(_prevState, currentState); + } + + _prevState = currentState; + } + else if (messageType != MessageType.Data) + { + // Invalid. + length = 0; + return; + } + + length = reader.AvailableBytes; + Buffer.BlockCopy(data, reader.Position, data, 0, length); + } + + public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + NetDataWriter writer = new NetDataWriter(true, length + EdgegapProtocol.ClientOverhead); + + writer.Put(_userAuthorizationToken); + writer.Put(_sessionAuthorizationToken); + + // Handle ping + if (length == 0) + writer.Put((byte)MessageType.Ping); + else if (_prevState == RelayConnectionState.Valid) + { + writer.Put((byte)MessageType.Data); + writer.Put(data, offset, length); + } else + length = 0; // Drop the packet if the relay isn't ready + + Buffer.BlockCopy(writer.Data, 0, data, 0, writer.Length); + length = writer.Length; + } + } +} diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientSocket.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientSocket.cs new file mode 100644 index 0000000..b879cd1 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapClientSocket.cs @@ -0,0 +1,366 @@ +using FishNet.Transporting.Tugboat; +using LiteNetLib; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using UnityEngine; + +namespace FishNet.Transporting.Edgegap.Client +{ + public class EdgegapClientSocket : EdgegapCommonSocket + { + ~EdgegapClientSocket() + { + StopConnection(); + } + + public RelayConnectionState relayConnectionState = RelayConnectionState.Disconnected; + + #region Private. + #region Configuration. + + /// + /// User authorization token for relay usage. + /// + private uint _userAuthorizationToken; + + /// + /// Session authorization token for relay usage. + /// + private uint _sessionAuthorizationToken; + + /// + /// MTU sizes for each channel. + /// + private int _mtu; + #endregion + #region Queues. + /// + /// Changes to the sockets local connection state. + /// + private Queue _localConnectionStates = new Queue(); + /// + /// Inbound messages which need to be handled. + /// + private Queue _incoming = new Queue(); + /// + /// Outbound messages which need to be handled. + /// + private Queue _outgoing = new Queue(); + #endregion + /// + /// Client socket manager. + /// + private NetManager _client; + /// + /// How long in seconds until client times from server. + /// + private int _timeout; + /// + /// PacketLayer to use with LiteNetLib. + /// + private EdgegapClientLayer _packetLayer; + /// + /// Locks the NetManager to stop it. + /// + private readonly object _stopLock = new object(); + + #endregion + + /// + /// Initializes this for use. + /// + /// + internal void Initialize(Transport t, int unreliableMTU) + { + base.Transport = t; + _mtu = unreliableMTU; + } + + /// + /// Updates the Timeout value as seconds. + /// + internal void UpdateTimeout(int timeout) + { + _timeout = timeout; + base.UpdateTimeout(_client, timeout); + } + + /// + /// Threaded operation to process client actions. + /// + private void ThreadedSocket() + { + EventBasedNetListener listener = new EventBasedNetListener(); + listener.NetworkReceiveEvent += Listener_NetworkReceiveEvent; + listener.PeerConnectedEvent += Listener_PeerConnectedEvent; + listener.PeerDisconnectedEvent += Listener_PeerDisconnectedEvent; + + // To support using the relay, the ClientLayer is supplied + _client = new NetManager(listener, _packetLayer); + _client.MtuOverride = (_mtu + NetConstants.FragmentedHeaderTotalSize); + + UpdateTimeout(_timeout); + + _localConnectionStates.Enqueue(LocalConnectionState.Starting); + _client.Start(); + + _client.Connect(_relay.Address.ToString(), _relay.Port, string.Empty); + } + + + /// + /// Stops the socket on a new thread. + /// + private void StopSocketOnThread() + { + if (_client == null) + return; + + Task t = Task.Run(() => + { + lock (_stopLock) + { + _client?.Stop(); + _client = null; + } + + //If not stopped yet also enqueue stop. + if (base.GetConnectionState() != LocalConnectionState.Stopped) + _localConnectionStates.Enqueue(LocalConnectionState.Stopped); + }); + } + + /// + /// Starts the client connection through the relay. + /// + /// + /// + /// + /// + internal bool StartConnection(string relayAddress, ushort relayPort, uint userAuthorizationToken, uint sessionAuthorizationToken, float pingInterval = EdgegapProtocol.PingInterval) + { + base.Initialize(relayAddress, relayPort, pingInterval); + if (base.GetConnectionState() != LocalConnectionState.Stopped) + return false; + + SetConnectionState(LocalConnectionState.Starting, false); + + //Assign properties. + _userAuthorizationToken = userAuthorizationToken; + _sessionAuthorizationToken = sessionAuthorizationToken; + + // Set up the relay layer + _packetLayer = new EdgegapClientLayer(userAuthorizationToken, sessionAuthorizationToken); + relayConnectionState = RelayConnectionState.Checking; + + // Track changes in the relay state + _packetLayer.OnStateChange += Listener_OnRelayStateChange; + + ResetQueues(); + Task t = Task.Run(() => ThreadedSocket()); + + return true; + } + + /// + /// Starts the client connection in direct P2P mode, bypassing the relay. + /// This can be used to reduce unecessary traffic through the relay. + /// + /// The address to connect + /// The local port to connect to + internal bool StartConnection(string address, ushort localPort) + { + base.Initialize(address, localPort, -1); + if (base.GetConnectionState() != LocalConnectionState.Stopped) + return false; + + SetConnectionState(LocalConnectionState.Starting, false); + + ResetQueues(); + Task t = Task.Run(() => ThreadedSocket()); + + return true; + } + + private void Listener_OnRelayStateChange(RelayConnectionState prev, RelayConnectionState current) + { + relayConnectionState = current; + + if (prev == current) return; + + // Mirror changes from relay state to client connection state + switch (current) + { + case RelayConnectionState.Valid: + Debug.Log("Client: Relay state is now Valid"); + _localConnectionStates.Enqueue(LocalConnectionState.Started); + break; + // Any other state should cause a disconnect + case RelayConnectionState.Invalid: + case RelayConnectionState.Error: + case RelayConnectionState.SessionTimeout: + case RelayConnectionState.Disconnected: + Debug.Log("Client: Bad relay state: " + current + ". Stopping..."); + StopConnection(); + break; + } + } + + + /// + /// Stops the local socket. + /// + internal bool StopConnection(DisconnectInfo? info = null) + { + if (base.GetConnectionState() == LocalConnectionState.Stopped || base.GetConnectionState() == LocalConnectionState.Stopping) + return false; + + if (info != null) + base.Transport.NetworkManager.Log($"Local client disconnect reason: {info.Value.Reason}."); + + base.SetConnectionState(LocalConnectionState.Stopping, false); + relayConnectionState = RelayConnectionState.Disconnected; + StopSocketOnThread(); + return true; + } + + /// + /// Resets queues. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetQueues() + { + _localConnectionStates.Clear(); + base.ClearPacketQueue(ref _incoming); + base.ClearPacketQueue(ref _outgoing); + } + + + /// + /// Called when disconnected from the server. + /// + private void Listener_PeerDisconnectedEvent(NetPeer peer, DisconnectInfo disconnectInfo) + { + StopConnection(disconnectInfo); + } + + /// + /// Called when connected to the server. + /// + private void Listener_PeerConnectedEvent(NetPeer peer) + { + _localConnectionStates.Enqueue(LocalConnectionState.Started); + } + + /// + /// Called when data is received from a peer. + /// + private void Listener_NetworkReceiveEvent(NetPeer fromPeer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + base.Listener_NetworkReceiveEvent(_incoming, fromPeer, reader, deliveryMethod, _mtu); + } + + /// + /// Dequeues and processes outgoing. + /// + private void DequeueOutgoing() + { + NetPeer peer = null; + if (_client != null) + peer = _client.FirstPeer; + //Server connection hasn't been made. + if (peer == null) + { + /* Only dequeue outgoing because other queues might have + * relevant information, such as the local connection queue. */ + base.ClearPacketQueue(ref _outgoing); + } + else + { + int count = _outgoing.Count; + for (int i = 0; i < count; i++) + { + Packet outgoing = _outgoing.Dequeue(); + + ArraySegment segment = outgoing.GetArraySegment(); + DeliveryMethod dm = (outgoing.Channel == (byte)Channel.Reliable) ? + DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable; + + //If over the MTU. + if (outgoing.Channel == (byte)Channel.Unreliable && segment.Count > _mtu) + { + base.Transport.NetworkManager.LogWarning($"Client is sending of {segment.Count} length on the unreliable channel, while the MTU is only {_mtu}. The channel has been changed to reliable for this send."); + dm = DeliveryMethod.ReliableOrdered; + } + + peer.Send(segment.Array, segment.Offset, segment.Count, dm); + + outgoing.Dispose(); + } + } + } + + /// + /// Allows for Outgoing queue to be iterated. + /// + internal void IterateOutgoing() + { + // IterateOutgoing is called for every tick, so we can use it to send the pings + base.OnTick(_client); + DequeueOutgoing(); + } + + /// + /// Iterates the Incoming queue. + /// + internal void IterateIncoming() + { + _client?.PollEvents(); + + /* Run local connection states first so we can begin + * to read for data at the start of the frame, as that's + * where incoming is read. */ + while (_localConnectionStates.Count > 0) + base.SetConnectionState(_localConnectionStates.Dequeue(), false); + + //Not yet started, cannot continue. + LocalConnectionState localState = base.GetConnectionState(); + if (localState != LocalConnectionState.Started) + { + ResetQueues(); + //If stopped try to kill task. + if (localState == LocalConnectionState.Stopped) + { + StopSocketOnThread(); + return; + } + } + + /* Incoming. */ + while (_incoming.Count > 0) + { + Packet incoming = _incoming.Dequeue(); + ClientReceivedDataArgs dataArgs = new ClientReceivedDataArgs( + incoming.GetArraySegment(), + (Channel)incoming.Channel, base.Transport.Index); + base.Transport.HandleClientReceivedDataArgs(dataArgs); + //Dispose of packet. + incoming.Dispose(); + } + } + + /// + /// Sends a packet to the server. + /// + internal void SendToServer(byte channelId, ArraySegment segment) + { + //Not started, cannot send. + if (base.GetConnectionState() != LocalConnectionState.Started) + return; + + Send(ref _outgoing, channelId, segment, -1, _mtu); + } + } +} diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapCommonSocket.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapCommonSocket.cs new file mode 100644 index 0000000..bd81908 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapCommonSocket.cs @@ -0,0 +1,38 @@ +using LiteNetLib; +using FishNet.Transporting.Tugboat; +using System.Net; + +namespace FishNet.Transporting.Edgegap +{ + public abstract class EdgegapCommonSocket : CommonSocket + { + private uint _lastPingTick = 0; + private float _pingInterval; + + protected IPEndPoint _relay; + + protected void Initialize(string relayAddress, ushort relayPort, float pingInterval) + { + _relay = NetUtils.MakeEndPoint(relayAddress, relayPort); + _pingInterval = pingInterval; + } + + protected void OnTick(NetManager manager) + { + if (_pingInterval <= 0) return; + + // Send pings + var timePassed = InstanceFinder.TimeManager.TimePassed(_lastPingTick); + if (manager != null && (timePassed > _pingInterval)) + { + _lastPingTick = InstanceFinder.TimeManager.Tick; + + // This hack is supported by the Relay Client/Server layers to detect outgoing pings + // It's necessary since there's no way to bypass our Client/Server layer code + // which would consider any packet a Data packet. + manager.SendRaw(new byte[0], 0, 0, _relay); + } + } + } + +} \ No newline at end of file diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapProtocol.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapProtocol.cs new file mode 100644 index 0000000..8eadc83 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapProtocol.cs @@ -0,0 +1,30 @@ +// relay protocol definitions +namespace FishNet.Transporting.Edgegap +{ + public enum RelayConnectionState : byte + { + Disconnected = 0, // until the user calls connect() + Checking = 1, // recently connected, validation in progress + Valid = 2, // validation succeeded + Invalid = 3, // validation rejected by tower + SessionTimeout = 4, // session owner timed out + Error = 5, // other error + } + + public enum MessageType : byte + { + Ping = 1, + Data = 2 + } + + public static class EdgegapProtocol + { + // MTU: relay adds up to 13 bytes of metadata in the worst case. + public const int ServerOverhead = 13; + public const int ClientOverhead = 9; + + // ping interval should be between 100 ms and 1 second. + // faster ping gives faster authentication, but higher bandwidth. + public const float PingInterval = 0.5f; + } +} diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerLayer.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerLayer.cs new file mode 100644 index 0000000..97b5842 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerLayer.cs @@ -0,0 +1,95 @@ +using LiteNetLib.Utils; +using System; +using System.Net; + +namespace FishNet.Transporting.Edgegap.Server +{ + public class EdgegapServerLayer : LiteNetLib.Layers.PacketLayerBase + { + private IPEndPoint _relay; + private uint _userAuthorizationToken; + private uint _sessionAuthorizationToken; + private RelayConnectionState _prevState = RelayConnectionState.Disconnected; + + public Action OnStateChange; + + public EdgegapServerLayer(IPEndPoint relay, uint userAuthorizationToken, uint sessionAuthorizationToken) + : base(EdgegapProtocol.ServerOverhead) + { + _userAuthorizationToken = userAuthorizationToken; + _sessionAuthorizationToken = sessionAuthorizationToken; + + _relay = relay; + } + + public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + // Don't process packets not being received from the relay + if ((!endPoint.Equals(_relay)) && endPoint.Port != 0) return; + + NetDataReader reader = new NetDataReader(data, offset, length); + + var messageType = (MessageType)reader.GetByte(); + if (messageType == MessageType.Ping) + { + var currentState = (RelayConnectionState)reader.GetByte(); + if (currentState != _prevState && OnStateChange != null) + OnStateChange(_prevState, currentState); + + _prevState = currentState; + + // No data + length = 0; + } else if (messageType == MessageType.Data) + { + var connectionId = reader.GetUInt(); + + // Change the endpoint to a "Virtual" endpoint managed by the relay + // Here the IP is used to store the connectionId and the port signifies it's a virtual endpoint + endPoint.Address = new IPAddress(connectionId); + endPoint.Port = 0; + + length = reader.AvailableBytes; + Buffer.BlockCopy(data, reader.Position, data, 0, length); + } else + length = 0; + } + + public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + // Don't process packets not being set to the relay + if ((!endPoint.Equals(_relay)) && endPoint.Port != 0) return; + + NetDataWriter writer = new NetDataWriter(true, length + EdgegapProtocol.ServerOverhead); + + writer.Put(_userAuthorizationToken); + writer.Put(_sessionAuthorizationToken); + + // Handle ping + if (length == 0) + { + writer.Put((byte)MessageType.Ping); + } + else + { + // No sense sending data if the connection isn't valid + if (_prevState != RelayConnectionState.Valid) return; + + writer.Put((byte)MessageType.Data); + +#pragma warning disable CS0618 // Type or member is obsolete + int peerId = (int)endPoint.Address.Address; +#pragma warning restore CS0618 + + writer.Put(peerId); + writer.Put(data, offset, length); + + // Send the packet to the relay + endPoint = _relay; + } + + Buffer.BlockCopy(writer.Data, 0, data, 0, writer.Length); + length = writer.Length; + } + } +} diff --git a/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerSocket.cs b/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerSocket.cs new file mode 100644 index 0000000..e5316d7 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/Core/EdgegapServerSocket.cs @@ -0,0 +1,526 @@ +using FishNet.Connection; +using FishNet.Managing; +using LiteNetLib; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using UnityEngine; +using FishNet.Transporting.Tugboat; +using FishNet.Transporting.Tugboat.Server; + +namespace FishNet.Transporting.Edgegap.Server +{ + public class EdgegapServerSocket : EdgegapCommonSocket + { + + #region Public. + public RelayConnectionState relayConnectionState = RelayConnectionState.Disconnected; + + /// + /// Gets the current ConnectionState of a remote client on the server. + /// + /// ConnectionId to get ConnectionState for. + internal RemoteConnectionState GetConnectionState(int connectionId) + { + NetPeer peer = GetNetPeer(connectionId, false); + if (peer == null || peer.ConnectionState != ConnectionState.Connected) + return RemoteConnectionState.Stopped; + else + return RemoteConnectionState.Started; + } + + #endregion + + #region Private. + #region Configuration. + /// + /// User authorization token for relay usage. + /// + private uint _userAuthorizationToken; + /// + /// Session authorization token for relay usage. + /// + private uint _sessionAuthorizationToken; + /// + /// The local port to bind + /// + private ushort _localPort; + /// + /// Maximum number of allowed clients. + /// + private int _maximumClients; + /// + /// MTU size per packet. + /// + private int _mtu; + #endregion + #region Queues. + /// + /// Changes to the sockets local connection state. + /// + private Queue _localConnectionStates = new Queue(); + /// + /// Inbound messages which need to be handled. + /// + private Queue _incoming = new Queue(); + /// + /// Outbound messages which need to be handled. + /// + private Queue _outgoing = new Queue(); + /// + /// ConnectionEvents which need to be handled. + /// + private Queue _remoteConnectionEvents = new Queue(); + #endregion + /// + /// Key required to connect. + /// + private string _key = string.Empty; + /// + /// How long in seconds until client times from server. + /// + private int _timeout; + /// + /// Server socket manager. + /// + private NetManager _server; + /// + /// PacketLayer to use with LiteNetLib. + /// + private EdgegapServerLayer _packetLayer; + /// + /// Locks the NetManager to stop it. + /// + private readonly object _stopLock = new object(); + #endregion + + ~EdgegapServerSocket() + { + StopConnection(); + } + + /// + /// Initializes this for use. + /// + /// + internal void Initialize(Transport t, int unreliableMTU) + { + base.Transport = t; + _mtu = unreliableMTU; + } + + /// + /// Updates the Timeout value as seconds. + /// + internal void UpdateTimeout(int timeout) + { + _timeout = timeout; + base.UpdateTimeout(_server, timeout); + } + + + /// + /// Threaded operation to process server actions. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThreadedSocket() + { + EventBasedNetListener listener = new EventBasedNetListener(); + listener.ConnectionRequestEvent += Listener_ConnectionRequestEvent; + listener.PeerConnectedEvent += Listener_PeerConnectedEvent; + listener.NetworkReceiveEvent += Listener_NetworkReceiveEvent; + listener.PeerDisconnectedEvent += Listener_PeerDisconnectedEvent; + + // To support using the relay, the ServertLayer is supplied + _server = new NetManager(listener, _packetLayer); + _server.MtuOverride = (_mtu + NetConstants.FragmentedHeaderTotalSize); + + UpdateTimeout(_timeout); + + _localConnectionStates.Enqueue(LocalConnectionState.Starting); + _server.Start(_localPort); + + relayConnectionState = RelayConnectionState.Checking; + } + + /// + /// Stops the socket on a new thread. + /// + private void StopSocketOnThread() + { + if (_server == null) + return; + + Task t = Task.Run(() => + { + lock (_stopLock) + { + _server?.Stop(); + _server = null; + } + + //If not stopped yet also enqueue stop. + if (base.GetConnectionState() != LocalConnectionState.Stopped) + _localConnectionStates.Enqueue(LocalConnectionState.Stopped); + + relayConnectionState = RelayConnectionState.Disconnected; + }); + } + + /// + /// Gets the address of a remote connection Id. + /// + /// + /// If the connectionId refers to a Virtual client communicating through the relay, + /// the resulting address will be fictitious. That should be OK, since whatever the reason + /// for getting the connection address, the communication should still occur through the current transport. + /// + /// + /// Returns string.empty if Id is not found. + internal string GetConnectionAddress(int connectionId) + { + if (GetConnectionState() != LocalConnectionState.Started) + { + string msg = "Server socket is not started."; + if (Transport == null) + NetworkManager.StaticLogWarning(msg); + else + Transport.NetworkManager.LogWarning(msg); + return string.Empty; + } + + NetPeer peer = GetNetPeer(connectionId, false); + if (peer == null) + { + Transport.NetworkManager.LogWarning($"Connection Id {connectionId} returned a null NetPeer."); + return string.Empty; + } + + return peer.EndPoint.Address.ToString(); + } + + /// + /// Returns a NetPeer for connectionId. + /// + /// + /// + private NetPeer GetNetPeer(int connectionId, bool connectedOnly) + { + if (_server != null) + { + NetPeer peer = _server.GetPeerById(connectionId); + if (connectedOnly && peer != null && peer.ConnectionState != ConnectionState.Connected) + peer = null; + + return peer; + } + else + { + return null; + } + } + + /// + /// Starts the server. + /// + internal bool StartConnection(string relayAddress, ushort relayPort, uint userAuthorizationToken, uint sessionAuthorizationToken, ushort localPort, int maximumClients, float pingInterval = EdgegapProtocol.PingInterval) + { + base.Initialize(relayAddress, relayPort, pingInterval); + + if (base.GetConnectionState() != LocalConnectionState.Stopped) + return false; + + + SetConnectionState(LocalConnectionState.Starting, true); + + //Assign properties. + _userAuthorizationToken = userAuthorizationToken; + _sessionAuthorizationToken = sessionAuthorizationToken; + _maximumClients = maximumClients; + + // local server + _localPort = localPort; + + // Set up the relay layer + _packetLayer = new EdgegapServerLayer(_relay, userAuthorizationToken, sessionAuthorizationToken); + _packetLayer.OnStateChange += Listener_OnRelayStateChange; + + ResetQueues(); + + Task t = Task.Run(() => ThreadedSocket()); + + return true; + } + private void Listener_OnRelayStateChange(RelayConnectionState prev, RelayConnectionState current) + { + relayConnectionState = current; + + if (prev == current) return; + + // Mirror changes from relay state to client connection state + switch (current) + { + case RelayConnectionState.Valid: + Debug.Log("Server: Relay state is now Valid"); + _localConnectionStates.Enqueue(LocalConnectionState.Started); + break; + // Any other state should cause a disconnect + case RelayConnectionState.Invalid: + case RelayConnectionState.Error: + case RelayConnectionState.SessionTimeout: + case RelayConnectionState.Disconnected: + Debug.Log("Server: Bad relay state. " + current + ". Stopping..."); + StopConnection(); + break; + } + } + + /// + /// Stops the local socket. + /// + internal bool StopConnection() + { + if (_server == null || base.GetConnectionState() == LocalConnectionState.Stopped || base.GetConnectionState() == LocalConnectionState.Stopping) + return false; + + _localConnectionStates.Enqueue(LocalConnectionState.Stopping); + StopSocketOnThread(); + return true; + } + + /// + /// Stops a remote client disconnecting the client from the server. + /// + /// ConnectionId of the client to disconnect. + internal bool StopConnection(int connectionId) + { + //Server isn't running. + if (_server == null || base.GetConnectionState() != LocalConnectionState.Started) + return false; + + NetPeer peer = GetNetPeer(connectionId, false); + if (peer == null) + return false; + + try + { + peer.Disconnect(); + } + catch + { + return false; + } + + return true; + } + + /// + /// Resets queues. + /// + private void ResetQueues() + { + _localConnectionStates.Clear(); + base.ClearPacketQueue(ref _incoming); + base.ClearPacketQueue(ref _outgoing); + _remoteConnectionEvents.Clear(); + } + + + /// + /// Called when a peer disconnects or times out. + /// + private void Listener_PeerDisconnectedEvent(NetPeer peer, DisconnectInfo disconnectInfo) + { + _remoteConnectionEvents.Enqueue(new RemoteConnectionEvent(false, peer.Id)); + } + + /// + /// Called when a peer completes connection. + /// + private void Listener_PeerConnectedEvent(NetPeer peer) + { + _remoteConnectionEvents.Enqueue(new RemoteConnectionEvent(true, peer.Id)); + } + + /// + /// Called when data is received from a peer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Listener_NetworkReceiveEvent(NetPeer fromPeer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + //If over the MTU. + if (reader.AvailableBytes > _mtu) + { + _remoteConnectionEvents.Enqueue(new RemoteConnectionEvent(false, fromPeer.Id)); + fromPeer.Disconnect(); + } + else + { + base.Listener_NetworkReceiveEvent(_incoming, fromPeer, reader, deliveryMethod, _mtu); + } + } + + + /// + /// Called when a remote connection request is made. + /// + private void Listener_ConnectionRequestEvent(ConnectionRequest request) + { + if (_server == null) + return; + + //At maximum peers. + if (_server.ConnectedPeersCount >= _maximumClients) + { + request.Reject(); + return; + } + + request.AcceptIfKey(_key); + } + + /// + /// Dequeues and processes outgoing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DequeueOutgoing() + { + if (base.GetConnectionState() != LocalConnectionState.Started || _server == null) + { + //Not started, clear outgoing. + base.ClearPacketQueue(ref _outgoing); + } + else + { + int count = _outgoing.Count; + for (int i = 0; i < count; i++) + { + Packet outgoing = _outgoing.Dequeue(); + int connectionId = outgoing.ConnectionId; + + ArraySegment segment = outgoing.GetArraySegment(); + DeliveryMethod dm = (outgoing.Channel == (byte)Channel.Reliable) ? + DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable; + + //If over the MTU. + if (outgoing.Channel == (byte)Channel.Unreliable && segment.Count > _mtu) + { + base.Transport.NetworkManager.LogWarning($"Server is sending of {segment.Count} length on the unreliable channel, while the MTU is only {_mtu}. The channel has been changed to reliable for this send."); + dm = DeliveryMethod.ReliableOrdered; + } + + //Send to all clients. + if (connectionId == NetworkConnection.UNSET_CLIENTID_VALUE) + { + _server.SendToAll(segment.Array, segment.Offset, segment.Count, dm); + } + //Send to one client. + else + { + NetPeer peer = GetNetPeer(connectionId, true); + //If peer is found. + if (peer != null) + peer.Send(segment.Array, segment.Offset, segment.Count, dm); + } + + outgoing.Dispose(); + } + } + } + + /// + /// Allows for Outgoing queue to be iterated. + /// + internal void IterateOutgoing() + { + // IterateOutgoing is called for every tick, so we can use it to send the pings + base.OnTick(_server); + DequeueOutgoing(); + } + + /// + /// Iterates the Incoming queue. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void IterateIncoming() + { + _server?.PollEvents(); + + /* Run local connection states first so we can begin + * to read for data at the start of the frame, as that's + * where incoming is read. */ + while (_localConnectionStates.Count > 0) + base.SetConnectionState(_localConnectionStates.Dequeue(), true); + + //Not yet started. + LocalConnectionState localState = base.GetConnectionState(); + if (localState != LocalConnectionState.Started) + { + ResetQueues(); + //If stopped try to kill task. + if (localState == LocalConnectionState.Stopped) + { + StopSocketOnThread(); + return; + } + } + + //Handle connection and disconnection events. + while (_remoteConnectionEvents.Count > 0) + { + RemoteConnectionEvent connectionEvent = _remoteConnectionEvents.Dequeue(); + RemoteConnectionState state = (connectionEvent.Connected) ? RemoteConnectionState.Started : RemoteConnectionState.Stopped; + base.Transport.HandleRemoteConnectionState(new RemoteConnectionStateArgs(state, connectionEvent.ConnectionId, base.Transport.Index)); + } + + //Handle packets. + while (_incoming.Count > 0) + { + Packet incoming = _incoming.Dequeue(); + //Make sure peer is still connected. + NetPeer peer = GetNetPeer(incoming.ConnectionId, true); + if (peer != null) + { + ServerReceivedDataArgs dataArgs = new ServerReceivedDataArgs( + incoming.GetArraySegment(), + (Channel)incoming.Channel, + incoming.ConnectionId, + base.Transport.Index); + + base.Transport.HandleServerReceivedDataArgs(dataArgs); + } + + incoming.Dispose(); + } + + } + + /// + /// Sends a packet to a single, or all clients. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SendToClient(byte channelId, ArraySegment segment, int connectionId) + { + Send(ref _outgoing, channelId, segment, connectionId, _mtu); + } + + /// + /// Returns the maximum number of clients allowed to connect to the server. If the transport does not support this method the value -1 is returned. + /// + /// + internal int GetMaximumClients() + { + return _maximumClients; + } + + /// + /// Sets the MaximumClients value. + /// + /// + internal void SetMaximumClients(int value) + { + _maximumClients = value; + } + } +} diff --git a/Unity/Fishnet/EdgegapTransport/EdgegapTransport.cs b/Unity/Fishnet/EdgegapTransport/EdgegapTransport.cs new file mode 100644 index 0000000..574bee4 --- /dev/null +++ b/Unity/Fishnet/EdgegapTransport/EdgegapTransport.cs @@ -0,0 +1,540 @@ +using FishNet.Managing; +using FishNet.Managing.Transporting; +using FishNet.Transporting.Tugboat.Client; +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace FishNet.Transporting.Edgegap +{ + [DisallowMultipleComponent] + [AddComponentMenu("FishNet/Transport/EdgegapTransport")] + public class EdgegapTransport : Transport + { + ~EdgegapTransport() + { + Shutdown(); + } + + #region Serialized. + [Header("Channels")] + /// + /// Maximum transmission unit for the unreliable channel. + /// + [Tooltip("Maximum transmission unit for the unreliable channel.")] + [Range(MINIMUM_UDP_MTU, MAXIMUM_UDP_MTU)] + [SerializeField] + private int _unreliableMTU = MAXIMUM_UDP_MTU; + + [Header("Relay")] + /// + /// The address of the relay + /// + [Tooltip("The address of the relay.")] + [SerializeField] + private string _relayAddress; + [Tooltip("The interval between ping messages to the relay.")] + [SerializeField] + private float _pingInterval = EdgegapProtocol.PingInterval; + + [Tooltip("The user authorization token.")] + [SerializeField] + private uint _userAuthorizationToken; + [Tooltip("The session authorization token.")] + [SerializeField] + private uint _sessionAuthorizationToken; + + [Header("Server")] + /// + /// Port to use. + /// + [Tooltip("Relay server port.")] + [SerializeField] + private ushort _relayServerPort; + [Tooltip("The port to bind on the local server")] + [SerializeField] + private ushort _localPort = 7770; + + + /// + /// Maximum number of players which may be connected at once. + /// + [Tooltip("Maximum number of players which may be connected at once.")] + [Range(1, 9999)] + [SerializeField] + private int _maximumClients = 4095; + + + [Header("Client")] + /// + /// Address to connect. + /// + [Tooltip("Relay client port")] + [SerializeField] + private ushort _relayClientPort; + [Tooltip("If set, the client will attempt a direct P2P connection with this address, bypassing the relay.")] + [SerializeField] + private string _clientAddress; + + + [Header("Misc")] + /// + /// How long in seconds until either the server or client socket must go without data before being timed out. Use 0f to disable timing out. + /// + [Tooltip("How long in seconds until either the server or client socket must go without data before being timed out. Use 0f to disable timing out.")] + [Range(0, MAX_TIMEOUT_SECONDS)] + [SerializeField] + private ushort _timeout = 15; + #endregion + + #region Private. + /// + /// Server socket and handler. + /// + private Server.EdgegapServerSocket _server = new Server.EdgegapServerSocket(); + /// + /// Client socket and handler. + /// + private Client.EdgegapClientSocket _client = new Client.EdgegapClientSocket(); + private ClientSocket _localClient = new ClientSocket(); + + #endregion + + #region Const. + private const ushort MAX_TIMEOUT_SECONDS = 1800; + /// + /// Minimum UDP packet size allowed. + /// + private const int MINIMUM_UDP_MTU = 576; + /// + /// Maximum UDP packet size allowed. + /// + private const int MAXIMUM_UDP_MTU = 1023; + #endregion + + #region Initialization and unity. + public override void Initialize(NetworkManager networkManager, int transportIndex) + { + base.Initialize(networkManager, transportIndex); + } + + protected void OnDestroy() + { + Shutdown(); + } + #endregion + + #region ConnectionStates. + /// + /// Gets the address of a remote connection Id. + /// + /// + /// + public override string GetConnectionAddress(int connectionId) + { + return _server.GetConnectionAddress(connectionId); + } + /// + /// Called when a connection state changes for the local client. + /// + public override event Action OnClientConnectionState; + /// + /// Called when a connection state changes for the local server. + /// + public override event Action OnServerConnectionState; + /// + /// Called when a connection state changes for a remote client. + /// + public override event Action OnRemoteConnectionState; + /// + /// Gets the current local ConnectionState. + /// + /// True if getting ConnectionState for the server. + public override LocalConnectionState GetConnectionState(bool server) + { + if (server) + return _server.GetConnectionState(); + else + return _client.GetConnectionState(); + } + /// + /// Gets the current ConnectionState of a remote client on the server. + /// + /// ConnectionId to get ConnectionState for. + public override RemoteConnectionState GetConnectionState(int connectionId) + { + return _server.GetConnectionState(connectionId); + } + /// + /// Handles a ConnectionStateArgs for the local client. + /// + /// + public override void HandleClientConnectionState(ClientConnectionStateArgs connectionStateArgs) + { + OnClientConnectionState?.Invoke(connectionStateArgs); + } + /// + /// Handles a ConnectionStateArgs for the local server. + /// + /// + public override void HandleServerConnectionState(ServerConnectionStateArgs connectionStateArgs) + { + OnServerConnectionState?.Invoke(connectionStateArgs); + UpdateTimeout(); + } + /// + /// Handles a ConnectionStateArgs for a remote client. + /// + /// + public override void HandleRemoteConnectionState(RemoteConnectionStateArgs connectionStateArgs) + { + OnRemoteConnectionState?.Invoke(connectionStateArgs); + } + #endregion + + #region Iterating. + /// + /// Processes data received by the socket. + /// + /// True to process data received on the server. + public override void IterateIncoming(bool server) + { + if (server) + _server.IterateIncoming(); + else + _client.IterateIncoming(); + } + + /// + /// Processes data to be sent by the socket. + /// + /// True to process data received on the server. + public override void IterateOutgoing(bool server) + { + if (server) + _server.IterateOutgoing(); + else + _client.IterateOutgoing(); + } + #endregion + + #region ReceivedData. + /// + /// Called when client receives data. + /// + public override event Action OnClientReceivedData; + /// + /// Handles a ClientReceivedDataArgs. + /// + /// + public override void HandleClientReceivedDataArgs(ClientReceivedDataArgs receivedDataArgs) + { + OnClientReceivedData?.Invoke(receivedDataArgs); + } + /// + /// Called when server receives data. + /// + public override event Action OnServerReceivedData; + /// + /// Handles a ClientReceivedDataArgs. + /// + /// + public override void HandleServerReceivedDataArgs(ServerReceivedDataArgs receivedDataArgs) + { + OnServerReceivedData?.Invoke(receivedDataArgs); + } + #endregion + + #region Sending. + /// + /// Sends to the server or all clients. + /// + /// Channel to use. + /// Data to send. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void SendToServer(byte channelId, ArraySegment segment) + { + SanitizeChannel(ref channelId); + _client.SendToServer(channelId, segment); + } + /// + /// Sends data to a client. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void SendToClient(byte channelId, ArraySegment segment, int connectionId) + { + SanitizeChannel(ref channelId); + _server.SendToClient(channelId, segment, connectionId); + } + #endregion + + #region Configuration. + + /// + /// How long in seconds until either the server or client socket must go without data before being timed out. + /// + /// True to get the timeout for the server socket, false for the client socket. + /// + public override float GetTimeout(bool asServer) + { + //Server and client uses the same timeout. + return (float)_timeout; + } + /// + /// Sets how long in seconds until either the server or client socket must go without data before being timed out. + /// + /// True to set the timeout for the server socket, false for the client socket. + public override void SetTimeout(float value, bool asServer) + { + _timeout = (ushort)value; + } + /// + /// Returns the maximum number of clients allowed to connect to the server. If the transport does not support this method the value -1 is returned. + /// + /// + public override int GetMaximumClients() + { + return _server.GetMaximumClients(); + } + /// + /// Sets maximum number of clients allowed to connect to the server. If applied at runtime and clients exceed this value existing clients will stay connected but new clients may not connect. + /// + /// + public override void SetMaximumClients(int value) + { + _maximumClients = value; + _server.SetMaximumClients(value); + } + + /// + /// Sets the adress and port/s of the relay. Doesn't affect the transport at runtime. + /// These can be retrieved using the Edgegap Relay API + /// + /// The IP address or fqdn of the relay to use + /// The port used to send client messages to the relay. Use 0 to ignore./param> + /// The port used to send server messages to the relay. Use 0 to ignore. + public void SetRelayEndpoint(string address, ushort clientPort = 0, ushort serverPort = 0) + { + _relayAddress = address; + _relayClientPort = clientPort != 0 ? clientPort : _relayClientPort; + _relayServerPort = serverPort != 0 ? serverPort : _relayClientPort; + } + + /// + /// Sets the interval between ping messages to the relay. + /// + /// The interval. The lower the interval the faster the authorization but the higher the overhead. + public void SetPingInterval(int interval) + { + _pingInterval = interval; + } + + /// + /// Sets the authorization headers required by the relay. + /// These can be retrieved using the Edgegap Relay API + /// + /// The user authorization token + /// The session authorization token + public void SetRelayAuth(uint userAuth, uint sessionAuth) + { + _userAuthorizationToken = userAuth; + _sessionAuthorizationToken = sessionAuth; + } + + /// + /// Sets which local port to use. + /// + /// + public override void SetPort(ushort port) + { + _localPort = port; + } + + /// + /// Gets which port to use. + /// + public override ushort GetPort() + { + return _localPort; + } + + /// + /// Sets which address the client will connect to. + /// This will make the client bypass the relay and connect directly to the address. + /// + /// + public override void SetClientAddress(string address) + { + _clientAddress = address; + } + /// + /// Gets which address the client will connect to. + /// + public override string GetClientAddress() + { + return _clientAddress; + } + + #endregion + #region Start and stop. + /// + /// Starts the local server or client using configured settings. + /// + /// True to start server. + public override bool StartConnection(bool server) + { + if (server) + return StartServer(); + else + { + return StartClient(); + } + } + + /// + /// Stops the local server or client. + /// + /// True to stop server. + public override bool StopConnection(bool server) + { + if (server) + return StopServer(); + else + return StopClient(); + } + + /// + /// Stops a remote client from the server, disconnecting the client. + /// + /// ConnectionId of the client to disconnect. + /// True to abrutly stop the client socket. The technique used to accomplish immediate disconnects may vary depending on the transport. + /// When not using immediate disconnects it's recommended to perform disconnects using the ServerManager rather than accessing the transport directly. + /// + public override bool StopConnection(int connectionId, bool immediately) + { + return _server.StopConnection(connectionId); + } + + /// + /// Stops both client and server. + /// + public override void Shutdown() + { + //Stops client then server connections. + StopConnection(false); + StopConnection(true); + } + + #region Privates. + /// + /// Starts server. + /// + private bool StartServer() + { + _server.Initialize(this, _unreliableMTU); + UpdateTimeout(); + return _server.StartConnection(_relayAddress, _relayServerPort, _userAuthorizationToken, _sessionAuthorizationToken, _localPort, _maximumClients); + } + + /// + /// Stops server. + /// + private bool StopServer() + { + if (_server == null) + return false; + else + return _server.StopConnection(); + } + + /// + /// Starts the client. + /// + /// + private bool StartClient() + { + _client.Initialize(this, _unreliableMTU); + UpdateTimeout(); + + if (_clientAddress != String.Empty) + return _client.StartConnection(_clientAddress, _localPort); + + return _client.StartConnection(_relayAddress, _relayClientPort, _userAuthorizationToken, _sessionAuthorizationToken); + } + + /// + /// When enabled, local host mode allows the local client to connect direcly to the local server + /// via localhost, thereby reducing traffic sent through the relay. + /// This cannot be changed once the client has been started. + /// + /// true to enable, false to disable + private void SetLocalHost(bool enable) + { + _clientAddress = enable ? "localhost" : null; + } + + /// + /// Updates clients timeout values. + /// + private void UpdateTimeout() + { + //If server is running set timeout to max. This is for host only. + //int timeout = (GetConnectionState(true) != LocalConnectionState.Stopped) ? MAX_TIMEOUT_SECONDS : _timeout; + int timeout = (Application.isEditor) ? MAX_TIMEOUT_SECONDS : _timeout; + _client.UpdateTimeout(timeout); + _server.UpdateTimeout(timeout); + } + /// + /// Stops the client. + /// + private bool StopClient() + { + if (_client == null) + return false; + else + return _client.StopConnection(); + } + #endregion + #endregion + + #region Channels. + /// + /// If channelId is invalid then channelId becomes forced to reliable. + /// + /// + private void SanitizeChannel(ref byte channelId) + { + if (channelId < 0 || channelId >= TransportManager.CHANNEL_COUNT) + { + NetworkManager.LogWarning($"Channel of {channelId} is out of range of supported channels. Channel will be defaulted to reliable."); + channelId = 0; + } + } + /// + /// Gets the MTU for a channel. This should take header size into consideration. + /// For example, if MTU is 1200 and a packet header for this channel is 10 in size, this method should return 1190. + /// + /// + /// + public override int GetMTU(byte channel) + { + return _unreliableMTU; + } + #endregion + + #region Editor. +#if UNITY_EDITOR + private void OnValidate() + { + if (_unreliableMTU < 0) + _unreliableMTU = MINIMUM_UDP_MTU; + else if (_unreliableMTU > MAXIMUM_UDP_MTU) + _unreliableMTU = MAXIMUM_UDP_MTU; + } +#endif + #endregion + } +}