[Add] FishNet

This commit is contained in:
2026-03-30 20:11:57 +07:00
parent ee793a3361
commit c22c08753a
1797 changed files with 197950 additions and 1 deletions
@@ -0,0 +1,37 @@
using System;
using UnityEngine;
namespace FishNet.Managing.Transporting
{
/// <summary>
/// When inherited from this may be used with the TransportManager to alter messages before they are sent and received.
/// </summary>
public abstract class IntermediateLayer : MonoBehaviour
{
/// <summary>
/// TransportManager associated with this script.
/// </summary>
public TransportManager TransportManager { get; private set; }
/// <summary>
/// Called when data is received.
/// </summary>
/// <param name = "src">Original data.</param>
/// <param name = "fromServer">True if receiving from the server, false if from a client.</param>
/// <returns>Modified data.</returns>
public abstract ArraySegment<byte> HandleIncoming(ArraySegment<byte> src, bool fromServer);
/// <summary>
/// Called when data is sent.
/// </summary>
/// <param name = "src">Original data.</param>
/// <param name = "toServer">True if sending to the server, false if to a client.</param>
/// <returns>Modified data.</returns>
public abstract ArraySegment<byte> HandleOutgoing(ArraySegment<byte> src, bool toServer);
/// <summary>
/// Initializes this IntermediateLayer for use.
/// </summary>
public virtual void InitializeOnce(TransportManager manager) => TransportManager = manager;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3f8a2e0f9aaea614887f5f7b15350e46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Managing/Transporting/IntermediateLayer.cs
uploadId: 866910
@@ -0,0 +1,388 @@
using FishNet.Connection;
using FishNet.Transporting;
using FishNet.Utility.Performance;
using System;
using System.Collections.Generic;
using UnityEngine;
// Thanks to TiToMoskito originally creating this as a Transport.
// https://github.com/TiToMoskito/FishyLatency
namespace FishNet.Managing.Transporting
{
[Serializable]
public class LatencySimulator
{
#region Types.
/// <summary>
/// A message affected by latency.
/// </summary>
private struct Message
{
public readonly int ConnectionId;
public readonly byte[] Data;
public readonly int Length;
public readonly float SendTime;
public Message(int connectionId, ArraySegment<byte> segment, float latency)
{
ConnectionId = connectionId;
SendTime = Time.unscaledTime + latency;
Length = segment.Count;
Data = ByteArrayPool.Retrieve(Length);
Buffer.BlockCopy(segment.Array, segment.Offset, Data, 0, Length);
}
public ArraySegment<byte> GetSegment()
{
return new(Data, 0, Length);
}
}
#endregion
#region Internal.
/// <summary>
/// True if latency can be simulated.
/// </summary>
internal bool CanSimulate => GetEnabled() && (GetLatency() > 0 || GetPacketLost() > 0 || GetOutOfOrder() > 0);
#endregion
#region Serialized
[Header("Settings")]
/// <summary>
///
/// </summary>
[Tooltip("True if latency simulator is enabled.")]
[SerializeField]
private bool _enabled;
/// <summary>
/// Gets the enabled value of simulator.
/// </summary>
public bool GetEnabled() => _enabled;
/// <summary>
/// Sets the enabled value of simulator.
/// </summary>
/// <param name = "value">New value.</param>
public void SetEnabled(bool value)
{
if (value == _enabled)
return;
_enabled = value;
Reset();
}
/// <summary>
/// </summary>
[Tooltip("True to add latency on clientHost as well.")]
[SerializeField]
private bool _simulateHost = true;
/// <summary>
/// Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.
/// </summary>
[Tooltip("Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.")]
[Range(0, 60000)]
[SerializeField]
private long _latency = 0;
/// <summary>
/// Gets the latency value.
/// </summary>
/// <returns></returns>
public long GetLatency() => _latency;
/// <summary>
/// Sets a new latency value.
/// </summary>
/// <param name = "value">Latency as milliseconds.</param>
public void SetLatency(long value) => _latency = value;
[Header("Unreliable")]
/// <summary>
/// Percentage of unreliable packets which should arrive out of order.
/// </summary>
[Tooltip("Percentage of unreliable packets which should arrive out of order.")]
[Range(0f, 1f)]
[SerializeField]
private double _outOfOrder = 0;
/// <summary>
/// Out of order chance, 1f is a 100% chance to occur.
/// </summary>
/// <returns></returns>
public double GetOutOfOrder() => _outOfOrder;
/// <summary>
/// Sets out of order chance. 1f is a 100% chance to occur.
/// </summary>
/// <param name = "value">New Value.</param>
public void SetOutOfOrder(double value) => _outOfOrder = value;
/// <summary>
/// Percentage of packets which should drop.
/// </summary>
[Tooltip("Percentage of packets which should drop.")]
[Range(0, 1)]
[SerializeField]
private double _packetLoss = 0;
/// <summary>
/// Gets packet loss chance. 1f is a 100% chance to occur.
/// </summary>
/// <returns></returns>
public double GetPacketLost() => _packetLoss;
/// <summary>
/// Sets packet loss chance. 1f is a 100% chance to occur.
/// </summary>
/// <param name = "value">New Value.</param>
public void SetPacketLoss(double value) => _packetLoss = value;
#endregion
#region Private
/// <summary>
/// Transport to send data on.
/// </summary>
private Transport _transport;
/// <summary>
/// Reliable messages to the server.
/// </summary>
private List<Message> _toServerReliable = new();
/// <summary>
/// Unreliable messages to the server.
/// </summary>
private List<Message> _toServerUnreliable = new();
/// <summary>
/// Reliable messages to clients.
/// </summary>
private List<Message> _toClientReliable = new();
/// <summary>
/// Unreliable messages to clients.
/// </summary>
private List<Message> _toClientUnreliable = new();
/// <summary>
/// NetworkManager for this instance.
/// </summary>
private NetworkManager _networkManager;
/// <summary>
/// Used to generate chances of latency.
/// </summary>
private readonly System.Random _random = new();
#endregion
#region Initialization and Unity
public void Initialize(NetworkManager manager, Transport transport)
{
_networkManager = manager;
_transport = transport;
}
#endregion
/// <summary>
/// Stops both client and server.
/// </summary>
public void Reset()
{
bool enabled = GetEnabled();
if (_transport != null && enabled)
{
IterateAndStore(_toServerReliable);
IterateAndStore(_toServerUnreliable);
IterateAndStore(_toClientReliable);
IterateAndStore(_toClientUnreliable);
}
void IterateAndStore(List<Message> messages)
{
foreach (Message m in messages)
{
_transport.SendToServer((byte)Channel.Reliable, m.GetSegment());
ByteArrayPool.Store(m.Data);
}
}
_toServerReliable.Clear();
_toServerUnreliable.Clear();
_toClientReliable.Clear();
_toClientUnreliable.Clear();
}
/// <summary>
/// Removes pending or held packets for a connection.
/// </summary>
/// <param name = "conn">Connection to remove pending packets for.</param>
public void RemovePendingForConnection(int connectionId)
{
// If not enabled exit early to save work.
if (!GetEnabled())
return;
RemoveFromCollection(_toServerUnreliable);
RemoveFromCollection(_toServerUnreliable);
RemoveFromCollection(_toClientReliable);
RemoveFromCollection(_toClientUnreliable);
void RemoveFromCollection(List<Message> c)
{
for (int i = 0; i < c.Count; i++)
{
if (c[i].ConnectionId == connectionId)
{
c.RemoveAt(i);
i--;
}
}
}
}
#region Simulation
/// <summary>
/// Returns long latency as a float.
/// </summary>
/// <param name = "ms"></param>
/// <returns></returns>
private float GetLatencyAsFloat()
{
return (float)(_latency / 1000f);
}
/// <summary>
/// Adds a packet for simulation.
/// </summary>
public void AddOutgoing(byte channelId, ArraySegment<byte> segment, bool toServer = true, int connectionId = -1)
{
/* If to not simulate for host see if this packet
* should be sent normally. */
if (!_simulateHost && _networkManager != null && _networkManager.IsHostStarted)
{
/* If going to the server and is host then
* it must be sent from clientHost. */
if (toServer)
{
_transport.SendToServer(channelId, segment);
return;
}
//Not to server, see if going to clientHost.
else
{
//If connId is the same as clientHost id.
if (_networkManager.ClientManager.Connection.ClientId == connectionId)
{
_transport.SendToClient(channelId, segment, connectionId);
return;
}
}
}
List<Message> collection;
Channel c = (Channel)channelId;
if (toServer)
collection = c == Channel.Reliable ? _toServerReliable : _toServerUnreliable;
else
collection = c == Channel.Reliable ? _toClientReliable : _toClientUnreliable;
float latency = GetLatencyAsFloat();
//If dropping check to add extra latency if reliable, or discard if not.
if (DropPacket())
{
if (c == Channel.Reliable)
{
latency += latency * 0.3f; //add extra for resend.
}
//If not reliable then return the segment array to pool.
else
{
return;
}
}
Message msg = new(connectionId, segment, latency);
int count = collection.Count;
if (c == Channel.Unreliable && count > 0 && OutOfOrderPacket(c))
collection.Insert(count - 1, msg);
else
collection.Add(msg);
}
/// <summary>
/// Simulates pending outgoing packets.
/// </summary>
/// <param name = "asServer">True to send data from the local server to clients, false to send from the local client to server.
public void IterateOutgoing(bool asServer)
{
if (_transport == null)
{
Reset();
return;
}
if (asServer)
{
IterateCollection(_toClientReliable, Channel.Reliable);
IterateCollection(_toClientUnreliable, Channel.Unreliable);
}
else
{
IterateCollection(_toServerReliable, Channel.Reliable);
IterateCollection(_toServerUnreliable, Channel.Unreliable);
}
void IterateCollection(List<Message> collection, Channel channel)
{
byte cByte = (byte)channel;
float unscaledTime = Time.unscaledTime;
int count = collection.Count;
int iterations = 0;
for (int i = 0; i < count; i++)
{
Message msg = collection[i];
//Not enough time has passed.
if (unscaledTime < msg.SendTime)
break;
if (asServer)
_transport.SendToClient(cByte, msg.GetSegment(), msg.ConnectionId);
else
_transport.SendToServer(cByte, msg.GetSegment());
iterations++;
}
if (iterations > 0)
{
for (int i = 0; i < iterations; i++)
ByteArrayPool.Store(collection[i].Data);
collection.RemoveRange(0, iterations);
}
}
_transport.IterateOutgoing(asServer);
}
/// <summary>
/// Returns if a packet should drop.
/// </summary>
/// <returns></returns>
private bool DropPacket()
{
return _packetLoss > 0d && _random.NextDouble() < _packetLoss;
}
/// <summary>
/// Returns if a packet should be out of order.
/// </summary>
/// <param name = "c"></param>
/// <returns></returns>
private bool OutOfOrderPacket(Channel c)
{
if (c == Channel.Reliable)
return false;
return _outOfOrder > 0d && _random.NextDouble() < _outOfOrder;
}
#endregion
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 82bfafe804acb534fbf04c88de6eeed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Managing/Transporting/LatencySimulator.cs
uploadId: 866910
@@ -0,0 +1,95 @@
using FishNet.Serializing;
using System;
using UnityEngine;
namespace FishNet.Managing.Transporting
{
internal class SplitReader
{
#region Private.
/// <summary>
/// Tick split is for.
/// Tick must be a negative value so that it's impossible for the first tick to align.
/// </summary>
private long _tick = -1;
/// <summary>
/// Expected number of splits.
/// </summary>
private int _expectedMessages;
/// <summary>
/// Number of splits received so far.
/// </summary>
private ushort _receivedMessages;
/// <summary>
/// Writer containing split packet combined.
/// </summary>
private PooledWriter _writer = WriterPool.Retrieve();
#endregion
internal SplitReader()
{
// Increase capacity to reduce the chance of resizing.
_writer.EnsureBufferCapacity(20000);
}
/// <summary>
/// Gets split header values.
/// </summary>
internal void GetHeader(PooledReader reader, out int expectedMessages)
{
expectedMessages = reader.ReadInt32();
}
/// <summary>
/// Combines split data.
/// </summary>
internal void Write(uint tick, PooledReader reader, int expectedMessages)
{
// New tick which means new split.
if (tick != _tick)
Reset(tick, expectedMessages);
/* This is just a guess as to how large the end
* message could be. If the writer is not the minimum
* of this length then resize it. */
int estimatedBufferSize = expectedMessages * 1500;
if (_writer.Capacity < estimatedBufferSize)
_writer.EnsureBufferCapacity(estimatedBufferSize);
/* Empty remainder of reader into the writer.
* It does not matter if parts of the reader
* contain data added after the split because
* once the split is fully combined the data
* is parsed as though it came in as one message,
* which is how data is normally read. */
ArraySegment<byte> data = reader.ReadArraySegment(reader.Remaining);
_writer.WriteArraySegment(data);
_receivedMessages++;
}
/// <summary>
/// Returns if all split messages have been received.
/// </summary>
/// <returns></returns>
internal ArraySegment<byte> GetFullMessage()
{
if (_receivedMessages < _expectedMessages)
{
return default;
}
else
{
ArraySegment<byte> segment = _writer.GetArraySegment();
Reset();
return segment;
}
}
private void Reset(uint tick = 0, int expectedMessages = 0)
{
_tick = tick;
_receivedMessages = 0;
_expectedMessages = expectedMessages;
_writer.Clear();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a1c06be1540b77842be35823aa54b19b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Managing/Transporting/SplitReader.cs
uploadId: 866910
@@ -0,0 +1,101 @@
using System.Collections.Generic;
using FishNet.Connection;
using FishNet.Transporting;
using FishNet.Transporting.Multipass;
using GameKit.Dependencies.Utilities;
using UnityEngine;
namespace FishNet.Managing.Transporting
{
/// <summary>
/// Communicates with the Transport to send and receive data.
/// </summary>
public sealed partial class TransportManager : MonoBehaviour
{
/// <summary>
/// Returns IsLocalTransport for the transportId, optionally checking against a connectionId.
/// </summary>
public bool IsLocalTransport(int transportId, int connectionId = NetworkConnection.UNSET_CLIENTID_VALUE)
{
if (Transport == null)
return false;
if (Transport is Multipass mp)
return mp.IsLocalTransport(transportId, connectionId);
else
return Transport.IsLocalTransport(connectionId);
}
/// <summary>
/// Gets transport on index.
/// Commonly index will be 0 unless using Multipass.
/// </summary>
/// <returns></returns>
public Transport GetTransport(int index)
{
// If using multipass try to find the correct transport.
if (Transport is Multipass mp)
{
return mp.GetTransport(index);
}
// Not using multipass.
else
{
return Transport;
}
}
/// <summary>
/// Gets transport of type T.
/// </summary>
/// <returns>Returns the found transport which is of type T. Returns default of T if not found.</returns>
public T GetTransport<T>() where T : Transport
{
// If using multipass try to find the correct transport.
if (Transport is Multipass mp)
{
if (typeof(T) == typeof(Multipass))
return (T)(object)mp;
else
return mp.GetTransport<T>();
}
// Not using multipass.
else
{
if (Transport.GetType() == typeof(T))
return (T)(object)Transport;
else
return default;
}
}
/// <summary>
/// Returns all transports configured on the TransportManager.
/// </summary>
/// <param name = "includeMultipass">True to add Multipass to the results if being used. When false and using Multipass only the transport specified within Multipass will be returned.</param>
/// <returns></returns>
/// <remarks>This returns a collection from cache.</remarks>
public List<Transport> GetAllTransports(bool includeMultipass)
{
List<Transport> results = CollectionCaches<Transport>.RetrieveList();
// If using multipass check all transports.
if (Transport is Multipass mp)
{
if (includeMultipass)
results.Add(Transport);
foreach (Transport t in mp.Transports)
results.Add(t);
}
// Not using multipass.
else
{
results.Add(Transport);
}
return results;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: aac8eab4e511d7e4dbc81eb74aea7f23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Managing/Transporting/TransportManager.QOL.cs
uploadId: 866910
@@ -0,0 +1,929 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
#define DEVELOPMENT
#endif
using FishNet.Connection;
using FishNet.Managing.Timing;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Transporting;
using FishNet.Transporting.Multipass;
using System;
using System.Collections.Generic;
using FishNet.Managing.Statistic;
using GameKit.Dependencies.Utilities;
using UnityEngine;
namespace FishNet.Managing.Transporting
{
/// <summary>
/// Communicates with the Transport to send and receive data.
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("FishNet/Manager/TransportManager")]
public sealed partial class TransportManager : MonoBehaviour
{
#region Types.
private struct DisconnectingClient
{
public uint Tick;
public NetworkConnection Connection;
public DisconnectingClient(uint tick, NetworkConnection connection)
{
Tick = tick;
Connection = connection;
}
}
#endregion
#region Public.
/// <summary>
/// Returns if an IntermediateLayer is in use.
/// </summary>
public bool HasIntermediateLayer => _intermediateLayer != null;
/// <summary>
/// Called before IterateOutgoing has started.
/// </summary>
internal event Action OnIterateOutgoingStart;
/// <summary>
/// Called after IterateOutgoing has completed.
/// </summary>
internal event Action OnIterateOutgoingEnd;
/// <summary>
/// Called before IterateIncoming has started. True for on server, false for on client.
/// </summary>
internal event Action<bool> OnIterateIncomingStart;
/// <summary>
/// Called after IterateIncoming has completed. True for on server, false for on client.
/// </summary>
internal event Action<bool> OnIterateIncomingEnd;
/// <summary>
/// The current Transport being used.
/// </summary>
[Tooltip("The current Transport being used.")]
public Transport Transport;
#endregion
#region Serialized.
/// <summary>
/// Layer used to modify data before it is sent or received.
/// </summary>
[Tooltip("Layer used to modify data before it is sent or received.")]
[SerializeField]
private IntermediateLayer _intermediateLayer;
/// <summary>
/// </summary>
[Tooltip("Latency simulation settings.")]
[SerializeField]
private LatencySimulator _latencySimulator = new();
/// <summary>
/// Latency simulation settings.
/// </summary>
public LatencySimulator LatencySimulator
{
get
{
// Shouldn't ever be null unless the user nullifies it.
if (_latencySimulator == null)
_latencySimulator = new();
return _latencySimulator;
}
}
#endregion
#region Private.
/// <summary>
/// NetworkConnections on the server which have to send data to clients.
/// </summary>
private List<NetworkConnection> _dirtyToClients = new();
/// <summary>
/// PacketBundles to send to the server.
/// </summary>
private List<PacketBundle> _toServerBundles = new();
/// <summary>
/// NetworkManager handling this TransportManager.
/// </summary>
private NetworkManager _networkManager;
/// <summary>
/// Clients which are pending disconnects.
/// </summary>
private List<DisconnectingClient> _disconnectingClients = new();
/// <summary>
/// Lowest MTU of all transports for channels.
/// </summary>
private int[] _lowestMtus;
/// <summary>
/// Lowest MTU of all transports of all channels.
/// </summary>
private int _lowestMtu = 0;
/// <summary>
/// Custom amount to reserve on the MTU.
/// </summary>
private int _customMtuReserve = MINIMUM_MTU_RESERVE;
/// <summary>
/// </summary>
private NetworkTrafficStatistics _networkTrafficStatistics;
#endregion
#region Consts.
/// <summary>
/// Number of bytes sent for PacketId.
/// </summary>
public const byte PACKETID_LENGTH = 2;
/// <summary>
/// Number of bytes sent for ObjectId.
/// </summary>
public const byte OBJECT_ID_LENGTH = 2;
/// <summary>
/// Number of bytes sent for ComponentIndex.
/// </summary>
public const byte COMPONENT_INDEX_LENGTH = 1;
/// <summary>
/// Number of bytes sent for Tick.
/// </summary>
public const byte UNPACKED_TICK_LENGTH = 4;
/// <summary>
/// Number of bytes sent for an unpacked size, such as a collection or array size.
/// </summary>
public const byte UNPACKED_SIZE_LENGTH = 4;
/// <summary>
/// Number of bytes sent to indicate split count.
/// </summary>
private const byte SPLIT_COUNT_LENGTH = 4;
/// <summary>
/// Number of bytes required for split data.
/// </summary>
/// // todo: This shouldn't have to include TickBytes but there is a parse error if it's not included. Figure out why.
public const byte SPLIT_INDICATOR_LENGTH = UNPACKED_TICK_LENGTH + PACKETID_LENGTH + SPLIT_COUNT_LENGTH;
/// <summary>
/// Number of channels supported.
/// </summary>
public const byte CHANNEL_COUNT = 2;
/// <summary>
/// MTU reserved for internal use.
/// 1 byte is used to specify channel in packets for transports that do not include channel within their packet header. This is transport dependent.
/// </summary>
public const int MINIMUM_MTU_RESERVE = 1;
/// <summary>
/// Value to use when a MTU could not be found.
/// </summary>
public const int INVALID_MTU = -1;
#endregion
/// <summary>
/// Initializes this script for use.
/// </summary>
internal void InitializeOnce_Internal(NetworkManager manager)
{
_networkManager = manager;
TryAddDefaultTransport();
Transport.Initialize(_networkManager, 0);
SetLowestMTUs();
InitializeToServerBundles();
manager.StatisticsManager.TryGetNetworkTrafficStatistics(out _networkTrafficStatistics);
manager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState;
manager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
if (_intermediateLayer != null)
_intermediateLayer.InitializeOnce(this);
#if DEVELOPMENT
_latencySimulator.Initialize(manager, Transport);
#endif
}
/// <summary>
/// Sets the lowest MTU values.
/// </summary>
private void SetLowestMTUs()
{
// Already set.
if (_lowestMtu != 0)
return;
/* At least one transport is required.
* Try to add default. If a transport is already
* specified the add method will just exit early. */
TryAddDefaultTransport();
int allLowest = int.MaxValue;
// Cache lowest Mtus.
_lowestMtus = new int[CHANNEL_COUNT];
for (byte i = 0; i < CHANNEL_COUNT; i++)
{
int channelLowest = int.MaxValue;
if (Transport is Multipass mp)
{
foreach (Transport t in mp.Transports)
{
int mtu = t.GetMTU(i);
if (mtu != INVALID_MTU)
channelLowest = Mathf.Min(channelLowest, mtu);
}
}
else
{
channelLowest = Transport.GetMTU(i);
}
_lowestMtus[i] = channelLowest;
_lowestMtu = Mathf.Min(allLowest, channelLowest);
}
}
/// <summary>
/// Adds the default transport if a transport is not yet specified.
/// </summary>
private void TryAddDefaultTransport()
{
if (Transport == null && !gameObject.TryGetComponent(out Transport))
Transport = gameObject.AddComponent<FishNet.Transporting.Tugboat.Tugboat>();
}
/// <summary>
/// Called when the local connection state changes for the client.
/// </summary>
private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj)
{
// Not stopped.
if (obj.ConnectionState != LocalConnectionState.Stopped)
return;
// Reset toServer data.
foreach (PacketBundle pb in _toServerBundles)
pb.Reset(resetSendLast: true);
}
/// <summary>
/// Called when the local connection state changes for the server.
/// </summary>
private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj)
{
// Not stopped.
if (obj.ConnectionState != LocalConnectionState.Stopped)
return;
// If no server is started just clear all dirtyToClients.
if (!_networkManager.ServerManager.IsAnyServerStarted())
{
_dirtyToClients.Clear();
return;
}
// Only one server is stopped, remove connections for that server.
int index = obj.TransportIndex;
List<NetworkConnection> clientsForIndex = CollectionCaches<NetworkConnection>.RetrieveList();
foreach (NetworkConnection conn in _dirtyToClients)
{
if (conn.TransportIndex == index)
clientsForIndex.Add(conn);
}
foreach (NetworkConnection conn in clientsForIndex)
_dirtyToClients.Remove(conn);
CollectionCaches<NetworkConnection>.Store(clientsForIndex);
}
///// <summary>
///// Gets port for the first transport, or client transport if using Multipass.
///// </summary>
// private ushort GetPort(bool asServer)
// {
// if (Transport is Multipass mp)
// {
// if (asServer)
// return mp.Transports[0].GetPort();
// else
// return mp.ClientTransport.GetPort();
// }
// else
// {
// return Transport.GetPort();
// }
// }
///// <summary>
///// Stops the local server or client connection.
///// </summary>
// internal bool StopConnection(bool asServer)
// {
// return Transport.StopConnection(asServer);
// }
///// <summary>
///// Starts the local server or client connection.
///// </summary>
// internal bool StartConnection(bool asServer)
// {
// return Transport.StartConnection(asServer);
// }
///// <summary>
///// Starts the local server or client connection.
///// </summary>
// internal bool StartConnection(string address, bool asServer)
// {
// return StartConnection(address, GetPort(asServer), asServer);
// }
///// <summary>
///// Starts the local server or client connection on the first transport or ClientTransport if using Multipass and as client.
///// </summary>
// internal bool StartConnection(string address, ushort port, bool asServer)
// {
// Transport t;
// if (Transport is Multipass mp)
// {
// if (asServer)
// t = mp.Transports[0];
// else
// t = mp.ClientTransport;
// }
// else
// {
// t = Transport;
// }
// /* SetServerBindAddress must be called explictly. Only
// * set address if for client. */
// if (!asServer)
// t.SetClientAddress(address);
// t.SetPort(port);
// return t.StartConnection(asServer);
// }
/// <summary>
/// Sets a connection from server to client dirty.
/// </summary>
/// <param name = "conn"></param>
internal void ServerDirty(NetworkConnection conn)
{
_dirtyToClients.Add(conn);
}
/// <summary>
/// Initializes ToServerBundles for use.
/// </summary>
private void InitializeToServerBundles()
{
/* For ease of use FishNet will always have
* only two channels, reliable and unreliable.
* Even if the transport only supports reliable
* also setup for unreliable. */
for (byte i = 0; i < CHANNEL_COUNT; i++)
{
int mtu = GetLowestMTU(i);
_toServerBundles.Add(new(_networkManager, mtu));
}
}
#region GetMTU.
/// <summary>
/// Returns MTU excluding reserve amount.
/// </summary>
private int GetMTUWithReserve(int mtu)
{
int value = mtu - MINIMUM_MTU_RESERVE - _customMtuReserve;
/* If MTU is extremely low then warn user.
* The number choosen has no significant value. */
if (value <= 100)
{
string msg = $"Available MTU of {mtu} is significantly low; an invalid MTU will be returned. Check transport settings, or reduce MTU reserve if you set one using {nameof(SetMTUReserve)}";
_networkManager.LogWarning(msg);
return INVALID_MTU;
}
return value;
}
/// <summary>
/// Sets a custom value to reserve for the internal buffers.
/// This value is also deducted from transport MTU when using GetMTU methods.
/// </summary>
/// <param name = "value">Value to use.</param>
public void SetMTUReserve(int value)
{
if ((_networkManager != null && _networkManager.IsClientStarted) || _networkManager.IsServerStarted)
{
_networkManager.LogError($"A custom MTU reserve cannot be set after the server or client have been started or connected.");
return;
}
if (value < MINIMUM_MTU_RESERVE)
{
_networkManager.Log($"MTU reserve {value} is below minimum value of {MINIMUM_MTU_RESERVE}. Value has been updated to {MINIMUM_MTU_RESERVE}.");
value = MINIMUM_MTU_RESERVE;
}
_customMtuReserve = value;
InitializeToServerBundles();
}
/// <summary>
/// Returns the current MTU reserve.
/// </summary>
/// <returns></returns>
public int GetMTUReserve() => _customMtuReserve;
/// <summary>
/// Returns the lowest MTU of all channels. When using multipass this will evaluate all transports within Multipass.
/// </summary>
/// <param name = "channel"></param>
/// <returns></returns>
public int GetLowestMTU()
{
SetLowestMTUs();
return GetMTUWithReserve(_lowestMtu);
}
/// <summary>
/// Returns the lowest MTU for a channel. When using multipass this will evaluate all transports within Multipass.
/// </summary>
/// <param name = "channel"></param>
/// <returns></returns>
public int GetLowestMTU(byte channel)
{
SetLowestMTUs();
return GetMTUWithReserve(_lowestMtus[channel]);
}
/// <summary>
/// Gets MTU on the current transport for channel.
/// </summary>
/// <param name = "channel">Channel to get MTU of.</param>
/// <returns></returns>
public int GetMTU(byte channel)
{
SetLowestMTUs();
int mtu = Transport.GetMTU(channel);
if (mtu == INVALID_MTU)
return mtu;
return GetMTUWithReserve(mtu);
}
/// <summary>
/// Gets MTU on the transportIndex for channel. This requires use of Multipass.
/// </summary>
/// <param name = "transportIndex">Index of the transport to get the MTU on.</param>
/// <param name = "channel">Channel to get MTU of.</param>
/// <returns></returns>
public int GetMTU(int transportIndex, byte channel)
{
if (Transport is Multipass mp)
{
int mtu = mp.GetMTU(channel, transportIndex);
if (mtu == INVALID_MTU)
return INVALID_MTU;
return GetMTUWithReserve(mtu);
}
// Using first/only transport.
else if (transportIndex == 0)
{
return GetMTU(channel);
}
// Unhandled.
else
{
_networkManager.LogWarning($"MTU cannot be returned with transportIndex because {typeof(Multipass).Name} is not in use.");
return -1;
}
}
/// <summary>
/// Gets MTU on the transport type for channel. This requires use of Multipass.
/// </summary>
/// <typeparam name = "T">Tyep of transport to use.</typeparam>
/// <param name = "channel">Channel to get MTU of.</param>
/// <returns></returns>
public int GetMTU<T>(byte channel) where T : Transport
{
Transport transport = GetTransport<T>();
if (transport != null)
{
int mtu = transport.GetMTU(channel);
if (mtu == INVALID_MTU)
return mtu;
return GetMTUWithReserve(mtu);
}
// Fall through.
return INVALID_MTU;
}
#endregion
/// <summary>
/// Passes received to the intermediate layer.
/// </summary>
internal ArraySegment<byte> ProcessIntermediateIncoming(ArraySegment<byte> src, bool fromServer)
{
return _intermediateLayer.HandleIncoming(src, fromServer);
}
/// <summary>
/// Passes sent to the intermediate layer.
/// </summary>
private ArraySegment<byte> ProcessIntermediateOutgoing(ArraySegment<byte> src, bool toServer)
{
return _intermediateLayer.HandleOutgoing(src, toServer);
}
/// <summary>
/// Sends data to a client.
/// </summary>
/// <param name = "channelId">Channel to send on.</param>
/// <param name = "segment">Data to send.</param>
/// <param name = "connection">Connection to send to. Use null for all clients.</param>
/// <param name = "splitLargeMessages">True to split large packets which exceed MTU and send them in order on the reliable channel.</param>
internal void SendToClient(byte channelId, ArraySegment<byte> segment, NetworkConnection connection, bool splitLargeMessages = true, DataOrderType orderType = DataOrderType.Default)
{
SetSplitValues(channelId, segment, splitLargeMessages, out int requiredMessages, out int maxSplitMessageSize);
SendToClient(channelId, segment, connection, requiredMessages, maxSplitMessageSize, orderType);
}
private void SendToClient(byte channelId, ArraySegment<byte> segment, NetworkConnection connection, int requiredSplitMessages, int maxSplitMessageSize, DataOrderType orderType = DataOrderType.Default)
{
if (connection == null)
return;
if (requiredSplitMessages > 1)
SendSplitData(connection, ref segment, requiredSplitMessages, maxSplitMessageSize, orderType);
else
connection.SendToClient(channelId, segment, false, orderType);
}
/// <summary>
/// Sends data to observers.
/// </summary>
internal void SendToClients(byte channelId, ArraySegment<byte> segment, HashSet<NetworkConnection> observers, HashSet<NetworkConnection> excludedConnections = null, bool splitLargeMessages = true, DataOrderType orderType = DataOrderType.Default)
{
SetSplitValues(channelId, segment, splitLargeMessages, out int requiredMessages, out int maxSplitMessageSize);
SendToClients(channelId, segment, observers, excludedConnections, requiredMessages, maxSplitMessageSize, orderType);
}
private void SendToClients(byte channelId, ArraySegment<byte> segment, HashSet<NetworkConnection> observers, HashSet<NetworkConnection> excludedConnections, int requiredSplitMessages, int maxSplitMessageSize, DataOrderType orderType = DataOrderType.Default)
{
if (excludedConnections == null || excludedConnections.Count == 0)
{
foreach (NetworkConnection conn in observers)
SendToClient(channelId, segment, conn, requiredSplitMessages, maxSplitMessageSize, orderType);
}
else
{
foreach (NetworkConnection conn in observers)
{
if (excludedConnections.Contains(conn))
continue;
SendToClient(channelId, segment, conn, requiredSplitMessages, maxSplitMessageSize, orderType);
}
}
}
/// <summary>
/// Sends data to all clients.
/// </summary>
/// <param name = "channelId">Channel to send on.</param>
/// <param name = "segment">Data to send.</param>
/// <param name = "splitLargeMessages">True to split large packets which exceed MTU and send them in order on the reliable channel.</param>
internal void SendToClients(byte channelId, ArraySegment<byte> segment, bool splitLargeMessages = true)
{
SetSplitValues(channelId, segment, splitLargeMessages, out int requiredMessages, out int maxSplitMessageSize);
SendToClients_Internal(channelId, segment, requiredMessages, maxSplitMessageSize);
}
private void SendToClients_Internal(byte channelId, ArraySegment<byte> segment, int requiredSplitMessages, int maxSplitMessageSize)
{
/* Rather than buffer the message once and send to every client
* it must be queued into every client. This ensures clients
* receive the message in order of other packets being
* delivered to them. */
foreach (NetworkConnection conn in _networkManager.ServerManager.Clients.Values)
SendToClient(channelId, segment, conn, requiredSplitMessages, maxSplitMessageSize);
}
/// <summary>
/// Sends data to the server.
/// </summary>
/// <param name = "channelId">Channel to send on.</param>
/// <param name = "segment">Data to send.</param>
/// <param name = "splitLargeMessages">True to split large packets which exceed MTU and send them in order on the reliable channel.</param>
internal void SendToServer(byte channelId, ArraySegment<byte> segment, bool splitLargeMessages = true, DataOrderType orderType = DataOrderType.Default)
{
SetSplitValues(channelId, segment, splitLargeMessages, out int requiredMessages, out int maxSplitMessageSize);
SendToServer(channelId, segment, requiredMessages, maxSplitMessageSize, orderType);
}
private void SendToServer(byte channelId, ArraySegment<byte> segment, int requiredMessages, int maxSplitMessageSize, DataOrderType orderType)
{
if (channelId >= _toServerBundles.Count)
channelId = (byte)Channel.Reliable;
if (requiredMessages > 1)
SendSplitData(null, ref segment, requiredMessages, maxSplitMessageSize, orderType);
else
_toServerBundles[channelId].Write(segment, false, orderType);
}
#region Splitting.
/// <summary>
/// Checks if a message can be split and outputs split information if so.
/// </summary>
private void SetSplitValues(byte channelId, ArraySegment<byte> segment, bool split, out int requiredMessages, out int maxSplitMessageSize)
{
if (!split)
{
requiredMessages = 0;
maxSplitMessageSize = 0;
}
else
{
SplitRequired(channelId, segment.Count, out requiredMessages, out maxSplitMessageSize);
}
}
/// <summary>
/// Checks to set channel to reliable if dataLength is too long.
/// </summary>
internal void CheckSetReliableChannel(int dataLength, ref Channel channel)
{
if (channel == Channel.Reliable)
return;
bool requiresMultipleMessages = GetRequiredMessageCount((byte)channel, dataLength, out _) > 1;
if (requiresMultipleMessages)
channel = Channel.Reliable;
}
/// <summary>
/// Gets the required number of messages needed for segmentSize and channel.
/// </summary>
private int GetRequiredMessageCount(byte channelId, int segmentSize, out int maxMessageSize)
{
maxMessageSize = GetLowestMTU(channelId) - SPLIT_INDICATOR_LENGTH;
return Mathf.CeilToInt((float)segmentSize / maxMessageSize);
}
/// <summary>
/// True if data must be split.
/// </summary>
/// <param name = "channelId"></param>
/// <param name = "segmentSize"></param>
private bool SplitRequired(byte channelId, int segmentSize, out int requiredMessages, out int maxMessageSize)
{
requiredMessages = GetRequiredMessageCount(channelId, segmentSize, out maxMessageSize);
bool splitRequired = requiredMessages > 1;
if (splitRequired && channelId != (byte)Channel.Reliable)
_networkManager.LogError($"A message of length {segmentSize} requires the reliable channel but was sent on channel {(Channel)channelId}. Please file this stack trace as a bug report.");
return splitRequired;
}
/// <summary>
/// Splits data going to which is too large to fit within the transport MTU.
/// </summary>
/// <param name = "conn">Connection to send to. If null data will be sent to the server.</param>
/// <returns>True if data was sent split.</returns>
private void SendSplitData(NetworkConnection conn, ref ArraySegment<byte> segment, int requiredMessages, int maxMessageSize, DataOrderType orderType)
{
if (requiredMessages <= 1)
{
_networkManager.LogError($"SendSplitData was called with {requiredMessages} required messages. This method should only be called if messages must be split into 2 pieces or more.");
return;
}
byte channelId = (byte)Channel.Reliable;
PooledWriter headerWriter = WriterPool.Retrieve();
headerWriter.WritePacketIdUnpacked(PacketId.Split);
headerWriter.WriteInt32(requiredMessages);
ArraySegment<byte> headerSegment = headerWriter.GetArraySegment();
int writeIndex = 0;
bool firstWrite = true;
// Send to connection until everything is written.
while (writeIndex < segment.Count)
{
int headerReduction = 0;
if (firstWrite)
{
headerReduction = headerSegment.Count;
firstWrite = false;
}
int chunkSize = Mathf.Min(segment.Count - writeIndex - headerReduction, maxMessageSize);
// Make a new array segment for the chunk that is getting split.
ArraySegment<byte> splitSegment = new(segment.Array, segment.Offset + writeIndex, chunkSize);
// If connection is specified then it's going to a client.
if (conn != null)
{
conn.SendToClient(channelId, headerSegment, true);
conn.SendToClient(channelId, splitSegment);
}
// Otherwise it's going to the server.
else
{
_toServerBundles[channelId].Write(headerSegment, true, orderType);
_toServerBundles[channelId].Write(splitSegment, false, orderType);
}
writeIndex += chunkSize;
}
headerWriter.Store();
}
#endregion
/// <summary>
/// Processes data received by the socket.
/// </summary>
/// <param name = "asServer">True to read data from clients, false to read data from the server.
internal void IterateIncoming(bool asServer)
{
OnIterateIncomingStart?.Invoke(asServer);
Transport.IterateIncoming(asServer);
OnIterateIncomingEnd?.Invoke(asServer);
}
/// <summary>
/// Processes data to be sent by the socket.
/// </summary>
/// <param name = "asServer">True to send data from the local server to clients, false to send from the local client to server.
internal void IterateOutgoing(bool asServer)
{
if (asServer && _networkManager.ServerManager.AreAllServersStopped())
return;
OnIterateOutgoingStart?.Invoke();
int channelCount = CHANNEL_COUNT;
ulong sentBytes = 0;
#if DEVELOPMENT
bool latencySimulatorEnabled = LatencySimulator.CanSimulate;
#endif
if (asServer)
SendAsServer();
else
SendAsClient();
// Sends data as server.
void SendAsServer()
{
TimeManager tm = _networkManager.TimeManager;
uint localTick = tm.LocalTick;
// Write any dirty syncTypes.
_networkManager.ServerManager.Objects.WriteDirtySyncTypes();
int dirtyCount = _dirtyToClients.Count;
// Run through all dirty connections to send data to.
for (int z = 0; z < dirtyCount; z++)
{
NetworkConnection conn = _dirtyToClients[z];
if (conn == null || !conn.IsValid)
continue;
// Get packets for every channel.
for (byte channel = 0; channel < channelCount; channel++)
{
if (conn.GetPacketBundle(channel, out PacketBundle pb))
{
ProcessPacketBundle(pb);
ProcessPacketBundle(pb.GetSendLastBundle(), true);
void ProcessPacketBundle(PacketBundle ppb, bool isLast = false)
{
for (int i = 0; i < ppb.WrittenBuffers; i++)
{
// Length should always be more than 0 but check to be safe.
if (ppb.GetBuffer(i, out ByteBuffer bb))
{
ArraySegment<byte> segment = new(bb.Data, 0, bb.Length);
if (HasIntermediateLayer)
segment = ProcessIntermediateOutgoing(segment, false);
#if DEVELOPMENT
if (latencySimulatorEnabled)
_latencySimulator.AddOutgoing(channel, segment, false, conn.ClientId);
else
#endif
Transport.SendToClient(channel, segment, conn.ClientId);
sentBytes += (ulong)segment.Count;
}
}
ppb.Reset(false);
}
}
}
/* When marked as disconnecting data will still be sent
* this iteration but the connection will be marked as invalid.
* This will prevent future data from going out/coming in.
* Also the connection will be added to a disconnecting collection
* so it will it disconnected briefly later to allow data from
* this tick to send. */
if (conn.Disconnecting)
{
uint requiredTicks = tm.TimeToTicks(0.1d, TickRounding.RoundUp);
/* Require 100ms or 2 ticks to pass
* before disconnecting to allow for the
* higher chance of success that remaining
* data is sent. */
requiredTicks = Math.Max(requiredTicks, 2);
_disconnectingClients.Add(new(requiredTicks + localTick, conn));
}
conn.ResetServerDirty();
}
// Iterate disconnects.
for (int i = 0; i < _disconnectingClients.Count; i++)
{
DisconnectingClient dc = _disconnectingClients[i];
if (localTick >= dc.Tick)
{
_networkManager.TransportManager.Transport.StopConnection(dc.Connection.ClientId, true);
_disconnectingClients.RemoveAt(i);
i--;
}
}
if (_networkTrafficStatistics != null)
_networkTrafficStatistics.AddOutboundSocketData(sentBytes, asServer: true);
if (dirtyCount == _dirtyToClients.Count)
_dirtyToClients.Clear();
else if (dirtyCount > 0)
_dirtyToClients.RemoveRange(0, dirtyCount);
}
// Sends data as client.
void SendAsClient()
{
for (byte channel = 0; channel < channelCount; channel++)
{
if (PacketBundle.GetPacketBundle(channel, _toServerBundles, out PacketBundle pb))
{
ProcessPacketBundle(pb);
ProcessPacketBundle(pb.GetSendLastBundle());
void ProcessPacketBundle(PacketBundle ppb)
{
for (int i = 0; i < ppb.WrittenBuffers; i++)
{
if (ppb.GetBuffer(i, out ByteBuffer bb))
{
ArraySegment<byte> segment = new(bb.Data, 0, bb.Length);
if (HasIntermediateLayer)
segment = ProcessIntermediateOutgoing(segment, true);
#if DEVELOPMENT
if (latencySimulatorEnabled)
_latencySimulator.AddOutgoing(channel, segment);
else
#endif
Transport.SendToServer(channel, segment);
sentBytes += (ulong)segment.Count;
}
}
ppb.Reset(false);
}
}
}
if (_networkTrafficStatistics != null)
_networkTrafficStatistics.AddOutboundSocketData(sentBytes, asServer: false);
}
#if DEVELOPMENT
if (latencySimulatorEnabled)
_latencySimulator.IterateOutgoing(asServer);
#endif
Transport.IterateOutgoing(asServer);
OnIterateOutgoingEnd?.Invoke();
}
#region Editor.
#if UNITY_EDITOR
private void OnValidate()
{
if (Transport == null)
Transport = GetComponent<Transport>();
/* Update enabled state to force a reset if needed.
* This may be required if the user checked the enabled
* tick box at runtime. If enabled value didn't change
* then the Get will be the same as the Set and nothing
* will happen. */
_latencySimulator.SetEnabled(_latencySimulator.GetEnabled());
}
#endif
#endregion
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 34e4a322dca349547989b14021da4e23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Managing/Transporting/TransportManager.cs
uploadId: 866910