2024-10-17 17:23:05 +03:00

441 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Mirror
{
// a transport that can listen to multiple underlying transport at the same time
[DisallowMultipleComponent]
public class MultiplexTransport : Transport, PortTransport
{
public Transport[] transports;
Transport available;
// underlying transport connectionId to multiplexed connectionId lookup.
//
// originally we used a formula to map the connectionId:
// connectionId * transportAmount + transportId
//
// if we have 3 transports, then
// transport 0 will produce connection ids [0, 3, 6, 9, ...]
// transport 1 will produce connection ids [1, 4, 7, 10, ...]
// transport 2 will produce connection ids [2, 5, 8, 11, ...]
//
// however, some transports like kcp may give very large connectionIds.
// if they are near int.max, then "* transprotAmount + transportIndex"
// will overflow, resulting in connIds which can't be projected back.
// https://github.com/vis2k/Mirror/issues/3280
//
// instead, use a simple lookup with 0-indexed ids.
// with initial capacity to avoid runtime allocations.
// (original connectionId, transport#) to multiplexed connectionId
readonly Dictionary<KeyValuePair<int, int>, int> originalToMultiplexedId =
new Dictionary<KeyValuePair<int, int>, int>(100);
// multiplexed connectionId to (original connectionId, transport#)
readonly Dictionary<int, KeyValuePair<int, int>> multiplexedToOriginalId =
new Dictionary<int, KeyValuePair<int, int>>(100);
// next multiplexed id counter. start at 1 because 0 is reserved for host.
int nextMultiplexedId = 1;
// prevent log flood from OnGUI or similar per-frame updates
bool alreadyWarned;
public ushort Port
{
get
{
foreach (Transport transport in transports)
if (transport.Available() && transport is PortTransport portTransport)
return portTransport.Port;
return 0;
}
set
{
if (Utils.IsHeadless() && !alreadyWarned)
{
// prevent log flood from OnGUI or similar per-frame updates
alreadyWarned = true;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Multiplexer] Server cannot set the same listen port for all transports! Set them directly instead.");
Console.ResetColor();
}
else
{
// We can't set the same port for all transports because
// listen ports have to be different for each transport
// so we just set the first available one.
// This depends on the selected build platform.
foreach (Transport transport in transports)
if (transport.Available() && transport is PortTransport portTransport)
{
portTransport.Port = value;
break;
}
}
}
}
// add to bidirection lookup. returns the multiplexed connectionId.
public int AddToLookup(int originalConnectionId, int transportIndex)
{
// add to both
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
int multiplexedId = nextMultiplexedId++;
originalToMultiplexedId[pair] = multiplexedId;
multiplexedToOriginalId[multiplexedId] = pair;
return multiplexedId;
}
public void RemoveFromLookup(int originalConnectionId, int transportIndex)
{
// remove from both
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId))
{
originalToMultiplexedId.Remove(pair);
multiplexedToOriginalId.Remove(multiplexedId);
}
}
public bool OriginalId(int multiplexId, out int originalConnectionId, out int transportIndex)
{
if (!multiplexedToOriginalId.ContainsKey(multiplexId))
{
originalConnectionId = 0;
transportIndex = 0;
return false;
}
KeyValuePair<int, int> pair = multiplexedToOriginalId[multiplexId];
originalConnectionId = pair.Key;
transportIndex = pair.Value;
return true;
}
public int MultiplexId(int originalConnectionId, int transportIndex)
{
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId))
return multiplexedId;
else
return 0;
}
////////////////////////////////////////////////////////////////////////
public void Awake()
{
if (transports == null || transports.Length == 0)
{
Debug.LogError("[Multiplexer] Multiplex transport requires at least 1 underlying transport");
}
}
public override void ClientEarlyUpdate()
{
foreach (Transport transport in transports)
transport.ClientEarlyUpdate();
}
public override void ServerEarlyUpdate()
{
foreach (Transport transport in transports)
transport.ServerEarlyUpdate();
}
public override void ClientLateUpdate()
{
foreach (Transport transport in transports)
transport.ClientLateUpdate();
}
public override void ServerLateUpdate()
{
foreach (Transport transport in transports)
transport.ServerLateUpdate();
}
void OnEnable()
{
foreach (Transport transport in transports)
transport.enabled = true;
}
void OnDisable()
{
foreach (Transport transport in transports)
transport.enabled = false;
}
public override bool Available()
{
// available if any of the transports is available
foreach (Transport transport in transports)
if (transport.Available())
return true;
return false;
}
#region Client
public override void ClientConnect(string address)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
available = transport;
transport.OnClientConnected = OnClientConnected;
transport.OnClientDataReceived = OnClientDataReceived;
transport.OnClientError = OnClientError;
transport.OnClientDisconnected = OnClientDisconnected;
transport.ClientConnect(address);
return;
}
}
throw new ArgumentException("[Multiplexer] No transport suitable for this platform");
}
public override void ClientConnect(Uri uri)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
try
{
available = transport;
transport.OnClientConnected = OnClientConnected;
transport.OnClientDataReceived = OnClientDataReceived;
transport.OnClientError = OnClientError;
transport.OnClientDisconnected = OnClientDisconnected;
transport.ClientConnect(uri);
return;
}
catch (ArgumentException)
{
// transport does not support the schema, just move on to the next one
}
}
}
throw new ArgumentException("[Multiplexer] No transport suitable for this platform");
}
public override bool ClientConnected()
{
return (object)available != null && available.ClientConnected();
}
public override void ClientDisconnect()
{
if ((object)available != null)
available.ClientDisconnect();
}
public override void ClientSend(ArraySegment<byte> segment, int channelId)
{
available.ClientSend(segment, channelId);
}
#endregion
#region Server
void AddServerCallbacks()
{
// all underlying transports should call the multiplex transport's events
for (int i = 0; i < transports.Length; i++)
{
// this is required for the handlers, if I use i directly
// then all the handlers will use the last i
int transportIndex = i;
Transport transport = transports[i];
transport.OnServerConnected = (originalConnectionId =>
{
// invoke Multiplex event with multiplexed connectionId
int multiplexedId = AddToLookup(originalConnectionId, transportIndex);
OnServerConnected.Invoke(multiplexedId);
});
transport.OnServerDataReceived = (originalConnectionId, data, channel) =>
{
// invoke Multiplex event with multiplexed connectionId
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
if (multiplexedId == 0)
{
if (Utils.IsHeadless())
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}");
Console.ResetColor();
}
else
Debug.LogWarning($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}");
return;
}
OnServerDataReceived.Invoke(multiplexedId, data, channel);
};
transport.OnServerError = (originalConnectionId, error, reason) =>
{
// invoke Multiplex event with multiplexed connectionId
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
if (multiplexedId == 0)
{
if (Utils.IsHeadless())
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}");
Console.ResetColor();
}
else
Debug.LogError($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}");
return;
}
OnServerError.Invoke(multiplexedId, error, reason);
};
transport.OnServerDisconnected = originalConnectionId =>
{
// invoke Multiplex event with multiplexed connectionId
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
if (multiplexedId == 0)
{
if (Utils.IsHeadless())
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}");
Console.ResetColor();
}
else
Debug.LogWarning($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}");
return;
}
OnServerDisconnected.Invoke(multiplexedId);
RemoveFromLookup(originalConnectionId, transportIndex);
};
}
}
// for now returns the first uri,
// should we return all available uris?
public override Uri ServerUri() =>
transports[0].ServerUri();
public override bool ServerActive()
{
// avoid Linq.All allocations
foreach (Transport transport in transports)
if (!transport.ServerActive())
return false;
return true;
}
public override string ServerGetClientAddress(int connectionId)
{
// convert multiplexed connectionId to original id & transport index
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
return transports[transportIndex].ServerGetClientAddress(originalConnectionId);
else
return "";
}
public override void ServerDisconnect(int connectionId)
{
// convert multiplexed connectionId to original id & transport index
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
transports[transportIndex].ServerDisconnect(originalConnectionId);
}
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
{
// convert multiplexed connectionId to original transport + connId
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
transports[transportIndex].ServerSend(originalConnectionId, segment, channelId);
}
public override void ServerStart()
{
AddServerCallbacks();
foreach (Transport transport in transports)
{
transport.ServerStart();
if (transport is PortTransport portTransport)
{
if (Utils.IsHeadless())
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}");
Console.ResetColor();
}
else
{
Debug.Log($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}");
}
}
}
}
public override void ServerStop()
{
foreach (Transport transport in transports)
transport.ServerStop();
}
#endregion
public override int GetMaxPacketSize(int channelId = 0)
{
// finding the max packet size in a multiplex environment has to be
// done very carefully:
// * servers run multiple transports at the same time
// * different clients run different transports
// * there should only ever be ONE true max packet size for everyone,
// otherwise a spawn message might be sent to all tcp sockets, but
// be too big for some udp sockets. that would be a debugging
// nightmare and allow for possible exploits and players on
// different platforms seeing a different game state.
// => the safest solution is to use the smallest max size for all
// transports. that will never fail.
int mininumAllowedSize = int.MaxValue;
foreach (Transport transport in transports)
{
int size = transport.GetMaxPacketSize(channelId);
mininumAllowedSize = Mathf.Min(size, mininumAllowedSize);
}
return mininumAllowedSize;
}
public override void Shutdown()
{
foreach (Transport transport in transports)
transport.Shutdown();
}
public override string ToString()
{
StringBuilder builder = new StringBuilder();
builder.Append("Multiplexer:");
foreach (Transport transport in transports)
builder.Append($" {transport}");
return builder.ToString().Trim();
}
}
}