#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Net.Sockets;
using System.Threading;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Network.DNS.Cache;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
///
/// Contains settings related to TCP Ringmaster, which manages and optimizes TCP connections.
///
public sealed class TCPRingmasterSettings
{
///
/// The maximum number of simultaneous TCP racers. Racers are used to establish and manage connections.
///
public int MaxSimultaneousRacers = 4;
///
/// Determines whether to shuffle addresses before assigning racing lanes.
///
public bool ShuffleAddresses = true;
///
/// Callback to implement a custom address shuffle algorithm. When assigned, no plugin-defined shuffle algorithm will be executed.
///
/// It must be thread-safe.
public Action CustomAddressShuffleAlgorithm;
///
/// The granularity of cancellation checking for TCP races. It specifies the time interval for checking if a race should be canceled.
///
public TimeSpan CancellationCheckingGranularity = TimeSpan.FromMilliseconds(100);
public override string ToString() => $"[{nameof(TCPRingmasterSettings)} {this.MaxSimultaneousRacers}, {this.ShuffleAddresses}, {this.CancellationCheckingGranularity}]";
}
///
/// Represents the result of a TCP race competition, including the winning socket or an error.
///
public sealed class TCPRaceResult
{
///
/// The socket that won the race competition, if available.
///
public Socket WinningSocket;
///
/// The error encountered during the race competition, if any.
///
public Exception Error;
///
/// Initializes a new instance of the class with the winning socket and an error, if any.
///
/// The winning socket of the race competition, if available.
/// The error encountered during the race competition, if any.
public TCPRaceResult(Socket socket, Exception ex)
{
this.WinningSocket = socket;
this.Error = ex;
}
public override string ToString() => $"[{nameof(TCPRaceResult)} {this.WinningSocket}, {this.Error}]";
}
///
/// Represents a TCP race competition with parameters and status.
///
sealed class Race
{
///
/// The parameters for the TCP race competition.
///
public TCPRaceParameters Parameters;
///
/// The index of the next address to connect to.
///
public int NextAddressIndex;
///
/// The number of running lanes in the competition.
///
public int RunningLanes;
public override string ToString() => $"[{nameof(Race)} {this.Parameters}, {this.NextAddressIndex}, {this.RunningLanes}]";
}
///
/// Represents a racing lane in a TCP race competition.
///
sealed class RacingLane
{
///
/// The associated race and its parameters.
///
public Race Race;
///
/// The index of the address to connect to.
///
public int AddressIndex;
///
/// The index of the racing lane.
///
public int LaneIndex;
///
/// The socket used for the racing lane.
///
public Socket Socket;
public override string ToString() => $"[{nameof(RacingLane)} {this.Race}, {this.AddressIndex}, {this.LaneIndex}, {this.Socket}]";
}
///
/// Contains parameters and settings for a TCP race competition to establish connections.
///
public sealed class TCPRaceParameters
{
///
/// An array of DNS IP addresses to be used for racing to establish a connection.
///
public DNSIPAddress[] Addresses;
///
/// The hostname to connect to.
///
public string Hostname;
///
/// The port to connect to.
///
public int Port;
///
/// The cancellation token used to cancel the TCP race competition.
///
public CancellationToken Token;
///
/// A callback function to announce the winner of the TCP race competition.
///
/// The TCPRaceParameters used for the race competition.
/// The result of the race competition, including the winning socket or an error.
public Action AnnounceWinnerCallback;
///
/// Optional context for logging and tracking purposes.
///
public LoggingContext Context;
///
/// A user-defined tag associated with the TCP race parameters.
///
public object Tag;
public override string ToString()
{
var sb = StringBuilderPool.Get(2 + this.Addresses.Length);
sb.Append('[');
for (int i = 0; i < this.Addresses.Length; ++i)
{
sb.Append(this.Addresses[i]);
if (i < this.Addresses.Length - 1)
sb.Append(", ");
}
sb.Append(']');
var ips = StringBuilderPool.ReleaseAndGrab(sb);
return $"[{nameof(TCPRaceParameters)} \"{this.Hostname}:{this.Port}\" Addresses({this.Addresses.Length}): {ips}]";
}
}
///
/// TCPRingmaster provides a method called , which is used to initiate a competition among multiple TCP connections.
/// Each connection attempt races against the others to establish a connection, and the first successful connection is considered the winner.
/// The class allows to specify a callback function (through ) that gets invoked when a winning connection is established or when the competition is canceled.
/// This callback can be used to take action based on the competition outcome.
/// Additionally it includes logic for optimizing the order in which connection attempts are made:
///
/// - It can shuffle the order of addresses to improve the chances of quickly finding a working address.
/// - It handles scenarios where some addresses may not be working and prioritizes working addresses.
///
///
///
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
public static class TCPRingmaster
{
///
/// Starts a TCP race competition to establish connections based on the provided parameters.
///
/// The parameters and settings for the TCP race competition.
public static void StartCompetion(TCPRaceParameters parameters)
{
// Initial idea:
// create Min(racers.Addresses.Length, Options.MaxSimultaneousRacers) lanes
// and increase RunningLanes
// randomize adresses
// assign one address to each lane, put the remaining addresses' index to the IndexQueue
// start connecting for each lane
var options = HTTPManager.PerHostSettings.Get(parameters.Hostname);
Race race = new Race
{
Parameters = parameters,
NextAddressIndex = 0,
RunningLanes = Math.Min(parameters.Addresses.Length, options.TCPRingmasterSettings.MaxSimultaneousRacers),
};
// Shuffle addresses?
var algo = options.TCPRingmasterSettings.CustomAddressShuffleAlgorithm;
if (algo != null)
{
try
{
algo(parameters);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(options.TCPRingmasterSettings.CustomAddressShuffleAlgorithm)}({parameters})", ex, parameters.Context);
}
}
else
{
// This helps in cases where the DNS query returns with a lot of addresses whos the first half isn't working.
// In this case, after shuffling there's better chance that a working address can be found in the first MaxSimultaneousRacers lanes.
if (options.TCPRingmasterSettings.ShuffleAddresses)
ShuffleAddresses(parameters.Addresses);
}
// Move non-working addresses to the end of the array, we will give them another chance
// but others will have higher priority
int nonWorkingCount = 0;
for (int i = 0; i < parameters.Addresses.Length - 1; i++)
{
var address = parameters.Addresses[i];
if (!address.IsWorkedLastTime)
{
Array.Copy(parameters.Addresses, i + 1, parameters.Addresses, i, parameters.Addresses.Length - i - 1);
parameters.Addresses[parameters.Addresses.Length - 1] = address;
if (++nonWorkingCount >= parameters.Addresses.Length)
break;
i--;
}
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(StartCompetion)}({parameters}) - creating {race.RunningLanes} lane(s)", parameters.Context);
int nextIdx = race.NextAddressIndex;
while (nextIdx < race.RunningLanes)
{
var address = race.Parameters.Addresses[nextIdx];
var socket = new Socket(address.IPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
var lane = new RacingLane
{
AddressIndex = nextIdx,
LaneIndex = nextIdx,
Race = race,
Socket = socket
};
var asyncResult = socket.BeginConnect(address.IPAddress, parameters.Port, OnLaneFinished, lane);
// Under Android (and possible under other non-windows platforms) Unity doesn't call the OnLaneFinished callback, only returns with the IAsyncResult instance.
if (asyncResult.CompletedSynchronously && asyncResult.IsCompleted)
OnLaneFinished(asyncResult);
nextIdx = Interlocked.Increment(ref lane.Race.NextAddressIndex);
}
if (parameters.Token != CancellationToken.None)
Extensions.Timer.Add(new Extensions.TimerData(options.TCPRingmasterSettings.CancellationCheckingGranularity, race, CheckForCanceled));
}
private static void OnLaneFinished(IAsyncResult ar)
{
var lane = ar.AsyncState as RacingLane;
try
{
// Lane callback logic:
// Increase CompletedRacers
// If there's an error completing the socket, or isn't connected,
// pick the next index and start to connect
// if no more racers (CompletedRacers == racers.Addresses.Length) and no more running lanes, call callback with error
// decrease RunningLanes
// Else If callback isn't null, call callback
lane.Socket.EndConnect(ar);
var callback = lane.Race.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange(ref lane.Race.Parameters.AnnounceWinnerCallback, null, callback);
if (callback != null)
{
var runningLanes = Interlocked.Decrement(ref lane.Race.RunningLanes);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Winner lane! Address: {lane.Race.Parameters.Addresses[lane.AddressIndex]}, remaining lanes: {runningLanes}", lane.Race.Parameters.Context);
try
{
callback.Invoke(lane.Race.Parameters, new TCPRaceResult(lane.Socket, null));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}({lane.LaneIndex}) - callback", ex, lane.Race.Parameters.Context);
}
}
else
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Late lane, disconnecting...", lane.Race.Parameters.Context);
// The callback is already called before this lane finished, we have no use for this socket, have to disconnect.
lane.Socket.Shutdown(SocketShutdown.Both);
lane.Socket.BeginDisconnect(false, OnSocketDisconnect, lane);
}
}
catch (InvalidOperationException)
{
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.endconnect#system-net-sockets-socket-endconnect(system-iasyncresult)
// EndConnect(IAsyncResult) was previously called for the asynchronous connection.
}
catch (Exception ex)
{
lane.Socket.Dispose();
DNSCache.ReportAsNonWorking(lane.Race.Parameters.Hostname, lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress, lane.Race.Parameters.Context);
var nextIndex = Interlocked.Increment(ref lane.Race.NextAddressIndex);
if (nextIndex < lane.Race.Parameters.Addresses.Length)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Couldn't connect to address, grabbing new index: {nextIndex}", lane.Race.Parameters.Context);
lane.AddressIndex = nextIndex;
lane.Socket = new Socket(lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
lane.Socket.BeginConnect(lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress, lane.Race.Parameters.Port, OnLaneFinished, lane);
}
else
{
// no more address to try, decrase the running lanes
var runningLanes = Interlocked.Decrement(ref lane.Race.RunningLanes);
if (runningLanes == 0)
{
// If no more lanes, try to call the callback.
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) Race is over, calling callback if it's still available", lane.Race.Parameters.Context);
var callback = lane.Race.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange(ref lane.Race.Parameters.AnnounceWinnerCallback, null, callback);
try
{
callback?.Invoke(lane.Race.Parameters, new TCPRaceResult(null, ex));
}
catch (Exception nex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - callback", nex, lane.Race.Parameters.Context);
}
}
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) Run out of free racers, there are still running racers({runningLanes}), retiring this lane.", lane.Race.Parameters.Context);
}
}
}
private static void OnSocketDisconnect(IAsyncResult ar)
{
var lane = ar.AsyncState as RacingLane;
try
{
lane.Socket.EndDisconnect(ar);
}
catch (Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnSocketDisconnect)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex})", ex, lane.Race.Parameters.Context);
}
finally
{
Interlocked.Decrement(ref lane.Race.RunningLanes);
lane.Socket.Dispose();
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnSocketDisconnect)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex})", lane.Race.Parameters.Context);
}
}
///
/// Inplace shuffles addresses.
///
/// The array of to shuffle.
public static void ShuffleAddresses(DNSIPAddress[] addresses)
{
var rand = new Random();
int n = addresses.Length;
while (n > 1)
{
int k = rand.Next(n--);
(addresses[n], addresses[k]) = (addresses[k], addresses[n]);
}
}
private static bool CheckForCanceled(DateTime now, object context)
{
var query = context as Race;
if (query.Parameters.Token.IsCancellationRequested)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(CheckForCanceled)}({query}) - Token.IsCancellationRequested!", query.Parameters.Context);
var callback = query.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange>(ref query.Parameters.AnnounceWinnerCallback, null, callback);
try
{
callback?.Invoke(query.Parameters, new TCPRaceResult(null, new TimeoutException("DNS Query Timed Out")));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(CheckForCanceled)}({query}) - callback", ex, query.Parameters.Context);
}
}
return query.Parameters.AnnounceWinnerCallback != null;
}
}
}
#endif