using FishNet.Authenticating;
using FishNet.Component.Observing;
using FishNet.Connection;
using FishNet.Managing.Debugging;
using FishNet.Managing.Logging;
using FishNet.Managing.Predicting;
using FishNet.Managing.Transporting;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Transporting;
using FishNet.Utility.Extension;
using FishNet.Utility.Performance;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace FishNet.Managing.Server
{
///
/// A container for server data and actions.
///
[DisallowMultipleComponent]
[AddComponentMenu("FishNet/Manager/ServerManager")]
public sealed partial class ServerManager : MonoBehaviour
{
#region Public.
///
/// Called after the local server connection state changes.
///
public event Action OnServerConnectionState;
///
/// Called when authenticator has concluded a result for a connection. Boolean is true if authentication passed, false if failed.
///
public event Action OnAuthenticationResult;
///
/// Called when a remote client state changes with the server.
///
public event Action OnRemoteConnectionState;
///
/// True if the server connection has started.
///
public bool Started { get; private set; }
///
/// Handling and information for objects on the server.
///
public ServerObjects Objects { get; private set; }
///
/// Authenticated and non-authenticated connected clients.
///
[HideInInspector]
public Dictionary Clients = new Dictionary();
///
/// NetworkManager for server.
///
[HideInInspector]
public NetworkManager NetworkManager { get; private set; }
#endregion
#region Serialized.
///
/// Authenticator for this ServerManager. May be null if not using authentication.
///
[Obsolete("Use GetAuthenticator and SetAuthenticator.")] //Remove on 2023/06/01
public Authenticator Authenticator
{
get => GetAuthenticator();
set => SetAuthenticator(value);
}
///
/// Gets the Authenticator for this manager.
///
///
public Authenticator GetAuthenticator() => _authenticator;
///
/// Gets the Authenticator for this manager, and initializes it.
///
///
public void SetAuthenticator(Authenticator value)
{
_authenticator = value;
InitializeAuthenticator();
}
[Tooltip("Authenticator for this ServerManager. May be null if not using authentication.")]
[SerializeField]
private Authenticator _authenticator;
///
/// Default send rate for SyncTypes. A value of 0f will send changed values every tick.
/// SyncTypeRate cannot yet be changed at runtime because this would require recalculating rates on SyncBase, which is not yet implemented.
///
///
internal float GetSynctypeRate() => _syncTypeRate;
[Tooltip("Default send rate for SyncTypes. A value of 0f will send changed values every tick.")]
[Range(0f, 60f)]
[SerializeField]
private float _syncTypeRate = 0.1f;
///
/// How to pack object spawns.
///
[Tooltip("How to pack object spawns.")]
[SerializeField]
internal TransformPackingData SpawnPacking = new TransformPackingData()
{
Position = AutoPackType.Unpacked,
Rotation = AutoPackType.PackedLess,
Scale = AutoPackType.PackedLess
};
///
/// True to automatically set the frame rate when the client connects.
///
[Tooltip("True to automatically set the frame rate when the client connects.")]
[SerializeField]
private bool _changeFrameRate = true;
///
/// Maximum frame rate the server may run at. When as host this value runs at whichever is higher between client and server.
///
internal ushort FrameRate => (_changeFrameRate) ? _frameRate : (ushort)0;
[Tooltip("Maximum frame rate the server may run at. When as host this value runs at whichever is higher between client and server.")]
[Range(1, NetworkManager.MAXIMUM_FRAMERATE)]
[SerializeField]
private ushort _frameRate = NetworkManager.MAXIMUM_FRAMERATE;
///
/// True to share the Ids of clients and the objects they own with other clients. No sensitive information is shared.
///
internal bool ShareIds => _shareIds;
[Tooltip("True to share the Ids of clients and the objects they own with other clients. No sensitive information is shared.")]
[SerializeField]
private bool _shareIds = true;
///
/// Gets StartOnHeadless value.
///
public bool GetStartOnHeadless() => _startOnHeadless;
///
/// Sets StartOnHeadless value.
///
/// New value to use.
public void SetStartOnHeadless(bool value) => _startOnHeadless = value;
[Tooltip("True to automatically start the server connection when running as headless.")]
[SerializeField]
private bool _startOnHeadless = true;
///
/// True to kick clients which send data larger than the MTU.
///
internal bool LimitClientMTU => _limitClientMTU;
[Tooltip("True to kick clients which send data larger than the MTU.")]
[SerializeField]
private bool _limitClientMTU = true;
#endregion
#region Private.
///
/// Used to read splits.
///
private SplitReader _splitReader = new SplitReader();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
///
/// Logs data about parser to help debug.
///
private ParseLogger _parseLogger = new ParseLogger();
#endif
#endregion
private void OnDestroy()
{
Objects?.SubscribeToSceneLoaded(false);
}
///
/// Initializes this script for use.
///
///
internal void InitializeOnce_Internal(NetworkManager manager)
{
NetworkManager = manager;
Objects = new ServerObjects(manager);
Objects.SubscribeToSceneLoaded(true);
InitializeRpcLinks();
//Unsubscribe first incase already subscribed.
SubscribeToTransport(false);
SubscribeToTransport(true);
NetworkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
NetworkManager.SceneManager.OnClientLoadedStartScenes += SceneManager_OnClientLoadedStartScenes;
if (_authenticator == null)
_authenticator = GetComponent();
if (_authenticator != null)
InitializeAuthenticator();
_cachedLevelOfDetailInterval = NetworkManager.ClientManager.LevelOfDetailInterval;
_cachedUseLod = NetworkManager.ObserverManager.GetUseNetworkLod();
}
///
/// Initializes the authenticator to this manager.
///
private void InitializeAuthenticator()
{
Authenticator auth = GetAuthenticator();
if (auth == null || auth.Initialized)
return;
if (NetworkManager == null)
return;
auth.InitializeOnce(NetworkManager);
auth.OnAuthenticationResult += _authenticator_OnAuthenticationResult;
}
///
/// Starts the server if configured to for headless.
///
internal void StartForHeadless()
{
if (GetStartOnHeadless())
{
//Wrapping logic in check instead of everything so _startOnHeadless doesnt warn as unused in editor.
#if UNITY_SERVER
StartConnection();
#endif
}
}
///
/// Stops the local server connection.
///
/// True to send a disconnect message to all clients first.
public bool StopConnection(bool sendDisconnectMessage)
{
if (sendDisconnectMessage)
SendDisconnectMessages(Clients.Values.ToList(), true);
//Return stop connection result.
return NetworkManager.TransportManager.Transport.StopConnection(true);
}
///
/// Sends a disconnect messge to connectionIds.
/// This does not iterate outgoing automatically.
///
///
internal void SendDisconnectMessages(int[] connectionIds)
{
List conns = new List();
foreach (int item in connectionIds)
{
if (Clients.TryGetValueIL2CPP(item, out NetworkConnection c))
conns.Add(c);
}
if (conns.Count > 0)
SendDisconnectMessages(conns, false);
}
///
/// Sends a disconnect message to all clients and immediately iterates outgoing.
///
private void SendDisconnectMessages(List conns, bool iterate)
{
PooledWriter writer = WriterPool.GetWriter();
writer.WritePacketId(PacketId.Disconnect);
ArraySegment segment = writer.GetArraySegment();
//Send segment to each client, authenticated or not.
foreach (NetworkConnection c in conns)
c.SendToClient((byte)Channel.Reliable, segment);
//Recycle writer.
writer.Dispose();
if (iterate)
NetworkManager.TransportManager.IterateOutgoing(true);
}
///
/// Starts the local server connection.
///
public bool StartConnection()
{
return NetworkManager.TransportManager.Transport.StartConnection(true);
}
///
/// Starts the local server using port.
///
/// Port to start on.
///
public bool StartConnection(ushort port)
{
Transport t = NetworkManager.TransportManager.Transport;
t.SetPort(port);
return t.StartConnection(true);
}
///
/// Called after the local client connection state changes.
///
private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj)
{
/* If client is doing anything but started destroy pending.
* Pending is only used for host mode. */
if (obj.ConnectionState != LocalConnectionState.Started)
Objects.DestroyPending();
}
///
/// Called when a client loads initial scenes after connecting.
///
private void SceneManager_OnClientLoadedStartScenes(NetworkConnection conn, bool asServer)
{
if (asServer)
{
Objects.RebuildObservers(conn);
/* If connection is host then renderers must be hidden
* for all objects not visible to the host. The observer system
* does handle this but only after an initial state is set.
* If the clientHost joins without observation of an object
* then the initial state will never be set. */
if (conn.IsLocalClient)
{
foreach (NetworkObject nob in Objects.Spawned.Values)
{
if (!nob.Observers.Contains(conn))
nob.SetRenderersVisible(false);
}
}
}
}
///
/// Changes subscription status to transport.
///
///
private void SubscribeToTransport(bool subscribe)
{
if (NetworkManager == null || NetworkManager.TransportManager == null || NetworkManager.TransportManager.Transport == null)
return;
if (subscribe)
{
NetworkManager.TransportManager.Transport.OnServerReceivedData += Transport_OnServerReceivedData;
NetworkManager.TransportManager.Transport.OnServerConnectionState += Transport_OnServerConnectionState;
NetworkManager.TransportManager.Transport.OnRemoteConnectionState += Transport_OnRemoteConnectionState;
}
else
{
NetworkManager.TransportManager.Transport.OnServerReceivedData -= Transport_OnServerReceivedData;
NetworkManager.TransportManager.Transport.OnServerConnectionState -= Transport_OnServerConnectionState;
NetworkManager.TransportManager.Transport.OnRemoteConnectionState -= Transport_OnRemoteConnectionState;
}
}
///
/// Called when authenticator has concluded a result for a connection. Boolean is true if authentication passed, false if failed.
/// Server listens for this event automatically.
///
private void _authenticator_OnAuthenticationResult(NetworkConnection conn, bool authenticated)
{
if (!authenticated)
conn.Disconnect(false);
else
ClientAuthenticated(conn);
}
///
/// Called when a connection state changes for the local server.
///
private void Transport_OnServerConnectionState(ServerConnectionStateArgs args)
{
/* Let the client manager know the server state is changing first.
* This gives the client an opportunity to clean-up or prepare
* before the server completes it's actions. */
Started = AnyServerStarted();
NetworkManager.ClientManager.Objects.OnServerConnectionState(args);
//If no servers are started then reset match conditions.
if (!Started)
{
MatchCondition.ClearMatchesWithoutRebuilding();
//Despawn without synchronizing network objects.
Objects.DespawnWithoutSynchronization(true);
}
Objects.OnServerConnectionState(args);
LocalConnectionState state = args.ConnectionState;
if (NetworkManager.CanLog(LoggingType.Common))
{
Transport t = NetworkManager.TransportManager.GetTransport(args.TransportIndex);
string tName = (t == null) ? "Unknown" : t.GetType().Name;
Debug.Log($"Local server is {state.ToString().ToLower()} for {tName}.");
}
NetworkManager.UpdateFramerate();
OnServerConnectionState?.Invoke(args);
}
///
/// Called when a connection state changes for a remote client.
///
private void Transport_OnRemoteConnectionState(RemoteConnectionStateArgs args)
{
//Sanity check to make sure transports are following proper types/ranges.
int id = args.ConnectionId;
int maxIdValue = short.MaxValue;
if (id < 0 || id > maxIdValue)
{
Kick(args.ConnectionId, KickReason.UnexpectedProblem, LoggingType.Error, $"The transport you are using supplied an invalid connection Id of {id}. Connection Id values must range between 0 and {maxIdValue}. The client has been disconnected.");
return;
}
//Valid Id.
else
{
//If started then add to authenticated clients.
if (args.ConnectionState == RemoteConnectionState.Started)
{
NetworkManager.Log($"Remote connection started for Id {id}.");
NetworkConnection conn = new NetworkConnection(NetworkManager, id, true);
Clients.Add(args.ConnectionId, conn);
OnRemoteConnectionState?.Invoke(conn, args);
//Connection is no longer valid. This can occur if the user changes the state using the OnRemoteConnectionState event.
if (!conn.IsValid)
return;
/* If there is an authenticator
* and the transport is not a local transport. */
Authenticator auth = GetAuthenticator();
if (auth != null && !NetworkManager.TransportManager.IsLocalTransport(id))
auth.OnRemoteConnection(conn);
else
ClientAuthenticated(conn);
}
//If stopping.
else if (args.ConnectionState == RemoteConnectionState.Stopped)
{
/* If client's connection is found then clean
* them up from server. */
if (Clients.TryGetValueIL2CPP(id, out NetworkConnection conn))
{
conn.SetDisconnecting(true);
OnRemoteConnectionState?.Invoke(conn, args);
Clients.Remove(id);
MatchCondition.RemoveFromMatchWithoutRebuild(conn, NetworkManager);
Objects.ClientDisconnected(conn);
BroadcastClientConnectionChange(false, conn);
//Return predictedObjectIds.
Queue pqId = conn.PredictedObjectIds;
while (pqId.Count > 0)
Objects.CacheObjectId(pqId.Dequeue());
conn.Reset();
NetworkManager.Log($"Remote connection stopped for Id {id}.");
}
}
}
}
///
/// Sends client their connectionId.
///
///
private void SendAuthenticated(NetworkConnection conn)
{
using (PooledWriter writer = WriterPool.GetWriter())
{
writer.WritePacketId(PacketId.Authenticated);
writer.WriteNetworkConnection(conn);
/* If predicted spawning is enabled then also send
* reserved objectIds. */
;
PredictionManager pm = NetworkManager.PredictionManager;
if (pm.GetAllowPredictedSpawning())
{
int count = Mathf.Min(Objects.GetObjectIdCache().Count, pm.GetReservedObjectIds());
writer.WriteByte((byte)count);
for (int i = 0; i < count; i++)
{
ushort val = (ushort)Objects.GetNextNetworkObjectId(false);
writer.WriteNetworkObjectId(val);
conn.PredictedObjectIds.Enqueue(val);
}
}
NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), conn);
}
}
///
/// Called when the server socket receives data.
///
private void Transport_OnServerReceivedData(ServerReceivedDataArgs args)
{
args.Data = NetworkManager.TransportManager.ProcessIntermediateIncoming(args.Data, false);
ParseReceived(args);
}
///
/// Called when the server receives data.
///
///
private void ParseReceived(ServerReceivedDataArgs args)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
_parseLogger.Reset();
#endif
//Not from a valid connection.
if (args.ConnectionId < 0)
return;
ArraySegment segment = args.Data;
NetworkManager.StatisticsManager.NetworkTraffic.LocalServerReceivedData((ulong)segment.Count);
if (segment.Count <= TransportManager.TICK_BYTES)
return;
//FishNet internally splits packets so nothing should ever arrive over MTU.
int channelMtu = NetworkManager.TransportManager.GetMTU(args.TransportIndex, (byte)args.Channel);
//If over MTU kick client immediately.
if (segment.Count > channelMtu && !NetworkManager.TransportManager.IsLocalTransport(args.ConnectionId))
{
ExceededMTUKick();
return;
}
PacketId packetId = PacketId.Unset;
#if !UNITY_EDITOR && !DEVELOPMENT_BUILD
try
{
#endif
using (PooledReader reader = ReaderPool.GetReader(segment, NetworkManager))
{
uint tick = reader.ReadUInt32(AutoPackType.Unpacked);
NetworkManager.TimeManager.LastPacketTick = tick;
/* This is a special condition where a message may arrive split.
* When this occurs buffer each packet until all packets are
* received. */
if (reader.PeekPacketId() == PacketId.Split)
{
//Skip packetId.
reader.ReadPacketId();
int expectedMessages;
_splitReader.GetHeader(reader, out expectedMessages);
//If here split message can be written.
_splitReader.Write(NetworkManager.TimeManager.LastPacketTick, reader, expectedMessages);
/* If fullMessage returns 0 count then the split
* has not written fully yet. Otherwise, if there is
* data within then reinitialize reader with the
* full message. */
ArraySegment fullMessage = _splitReader.GetFullMessage();
if (fullMessage.Count == 0)
return;
/* If here then all data has been received.
* It's possible the client could have exceeded
* maximum MTU but not the maximum number of splits.
* This is because the length of each split
* is not written, so we don't know how much data of the
* final message actually belonged to the split vs
* unrelated data added afterwards. We're going to cut
* the client some slack in this situation for the sake
* of keeping things simple. */
//Initialize reader with full message.
reader.Initialize(fullMessage, NetworkManager);
}
//Parse reader.
while (reader.Remaining > 0)
{
packetId = reader.ReadPacketId();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
_parseLogger.AddPacket(packetId);
#endif
NetworkConnection conn;
/* Connection isn't available. This should never happen.
* Force an immediate disconnect. */
if (!Clients.TryGetValueIL2CPP(args.ConnectionId, out conn))
{
Kick(args.ConnectionId, KickReason.UnexpectedProblem, LoggingType.Error, $"ConnectionId {conn.ClientId} not found within Clients. Connection will be kicked immediately.");
return;
}
conn.SetLastPacketTick(tick);
/* If connection isn't authenticated and isn't a broadcast
* then disconnect client. If a broadcast then process
* normally; client may still become disconnected if the broadcast
* does not allow to be called while not authenticated. */
if (!conn.Authenticated && packetId != PacketId.Broadcast)
{
conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a Broadcast without being authenticated. Connection will be kicked immediately.");
return;
}
//Only check if not developer build because users pay pause editor.
#if !DEVELOPMENT_BUILD && !UNITY_EDITOR
/* If hasn't sent LOD recently enough. LODs are sent every half a second, so
* by multiplaying interval by 60 this gives the client a 30 second window. */
if (_cachedUseLod && conn.IsLateForLevelOfDetail(_cachedLevelOfDetailInterval * 60))
{
conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} has gone too long without sending a level of detail update. Connection will be kicked immediately.");
return;
}
#endif
if (packetId == PacketId.Replicate)
{
Objects.ParseReplicateRpc(reader, conn, args.Channel);
}
else if (packetId == PacketId.ServerRpc)
{
Objects.ParseServerRpc(reader, conn, args.Channel);
}
else if (packetId == PacketId.ObjectSpawn)
{
if (!NetworkManager.PredictionManager.GetAllowPredictedSpawning())
{
conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a predicted spawn while predicted spawning is not enabled. Connection will be kicked immediately.");
return;
}
Objects.ReadPredictedSpawn(reader, conn);
}
else if (packetId == PacketId.ObjectDespawn)
{
if (!NetworkManager.PredictionManager.GetAllowPredictedSpawning())
{
conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {conn.ClientId} sent a predicted spawn while predicted spawning is not enabled. Connection will be kicked immediately.");
return;
}
Objects.ReadPredictedDespawn(reader, conn);
}
else if (packetId == PacketId.NetworkLODUpdate)
{
ParseNetworkLODUpdate(reader, conn);
}
else if (packetId == PacketId.Broadcast)
{
ParseBroadcast(reader, conn, args.Channel);
}
else if (packetId == PacketId.PingPong)
{
ParsePingPong(reader, conn);
}
else
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
NetworkManager.LogError($"Server received an unhandled PacketId of {(ushort)packetId} from connectionId {args.ConnectionId}. Remaining data has been purged.");
_parseLogger.Print(NetworkManager);
#else
NetworkManager.LogError($"Server received an unhandled PacketId of {(ushort)packetId} from connectionId {args.ConnectionId}. Connection will be kicked immediately.");
NetworkManager.TransportManager.Transport.StopConnection(args.ConnectionId, true);
#endif
return;
}
}
}
#if !UNITY_EDITOR && !DEVELOPMENT_BUILD
}
catch (Exception e)
{
Kick(args.ConnectionId, KickReason.MalformedData, LoggingType.Error, $"Server encountered an error while parsing data for packetId {packetId} from connectionId {args.ConnectionId}. Connection will be kicked immediately. Message: {e.Message}.");
}
#endif
//Kicks connection for exceeding MTU.
void ExceededMTUKick()
{
Kick(args.ConnectionId, KickReason.ExploitExcessiveData, LoggingType.Common, $"ConnectionId {args.ConnectionId} sent a message larger than allowed amount. Connection will be kicked immediately.");
}
}
///
/// Parses a received PingPong.
///
///
///
private void ParsePingPong(PooledReader reader, NetworkConnection conn)
{
/* //security limit how often clients can send pings.
* have clients use a stopwatch rather than frame time
* for checks to ensure it's not possible to send
* excessively should their game stutter then catch back up. */
uint clientTick = reader.ReadUInt32(AutoPackType.Unpacked);
if (conn.CanPingPong())
NetworkManager.TimeManager.SendPong(conn, clientTick);
}
///
/// Called when a remote client authenticates with the server.
///
///
private void ClientAuthenticated(NetworkConnection connection)
{
/* Immediately send connectionId to client. Some transports
* don't give clients their remoteId, therefor it has to be sent
* by the ServerManager. This packet is very simple and can be built
* on the spot. */
connection.ConnectionAuthenticated();
/* Send client Ids before telling the client
* they are authenticated. This is important because when the client becomes
* authenticated they set their LocalConnection using Clients field in ClientManager,
* which is set after getting Ids. */
BroadcastClientConnectionChange(true, connection);
SendAuthenticated(connection);
OnAuthenticationResult?.Invoke(connection, true);
NetworkManager.SceneManager.OnClientAuthenticated(connection);
}
///
/// Sends a client connection state change to owner and other clients if applicable.
///
private void BroadcastClientConnectionChange(bool connected, NetworkConnection conn)
{
//If sharing Ids then send all connected client Ids first if is a connected state.
if (ShareIds)
{
/* Send a broadcast to all authenticated clients with the clientId
* that just connected. The conn client will also get this. */
ClientConnectionChangeBroadcast changeMsg = new ClientConnectionChangeBroadcast()
{
Connected = connected,
Id = conn.ClientId
};
Broadcast(changeMsg);
/* If state is connected then the conn client
* must also receive all currently connected client ids. */
if (connected)
{
//Send already connected clients to the connection that just joined.
ListCache lc = ListCaches.GetIntCache();
foreach (int key in Clients.Keys)
lc.AddValue(key);
ConnectedClientsBroadcast allMsg = new ConnectedClientsBroadcast()
{
ListCache = lc
};
conn.Broadcast(allMsg);
ListCaches.StoreCache(lc);
}
}
//If not sharing Ids then only send ConnectionChange to conn.
else
{
if (connected)
{
/* Send broadcast only to the client which just disconnected.
* Only send if connecting. If the client is disconnected there's no reason
* to send them a disconnect msg. */
ClientConnectionChangeBroadcast changeMsg = new ClientConnectionChangeBroadcast()
{
Connected = connected,
Id = conn.ClientId
};
Broadcast(conn, changeMsg, true, Channel.Reliable);
}
}
}
}
}