- Initial commit

This commit is contained in:
Timur Kozanov
2026-07-03 05:02:26 +03:00
commit 374ff1e689
4080 changed files with 388870 additions and 0 deletions

View File

@ -0,0 +1,448 @@
#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
{
/// <summary>
/// Contains settings related to TCP Ringmaster, which manages and optimizes TCP connections.
/// </summary>
public sealed class TCPRingmasterSettings
{
/// <summary>
/// The maximum number of simultaneous TCP racers. Racers are used to establish and manage connections.
/// </summary>
public int MaxSimultaneousRacers = 4;
/// <summary>
/// Determines whether to shuffle addresses before assigning racing lanes.
/// </summary>
public bool ShuffleAddresses = true;
/// <summary>
/// Callback to implement a custom address shuffle algorithm. When assigned, no plugin-defined shuffle algorithm will be executed.
/// </summary>
/// <remarks>It must be thread-safe.</remarks>
public Action<TCPRaceParameters> CustomAddressShuffleAlgorithm;
/// <summary>
/// The granularity of cancellation checking for TCP races. It specifies the time interval for checking if a race should be canceled.
/// </summary>
public TimeSpan CancellationCheckingGranularity = TimeSpan.FromMilliseconds(100);
public override string ToString() => $"[{nameof(TCPRingmasterSettings)} {this.MaxSimultaneousRacers}, {this.ShuffleAddresses}, {this.CancellationCheckingGranularity}]";
}
/// <summary>
/// Represents the result of a TCP race competition, including the winning socket or an error.
/// </summary>
public sealed class TCPRaceResult
{
/// <summary>
/// The socket that won the race competition, if available.
/// </summary>
public Socket WinningSocket;
/// <summary>
/// The error encountered during the race competition, if any.
/// </summary>
public Exception Error;
/// <summary>
/// Initializes a new instance of the <see cref="TCPRaceResult"/> class with the winning socket and an error, if any.
/// </summary>
/// <param name="socket">The winning socket of the race competition, if available.</param>
/// <param name="ex">The error encountered during the race competition, if any.</param>
public TCPRaceResult(Socket socket, Exception ex)
{
this.WinningSocket = socket;
this.Error = ex;
}
public override string ToString() => $"[{nameof(TCPRaceResult)} {this.WinningSocket}, {this.Error}]";
}
/// <summary>
/// Represents a TCP race competition with parameters and status.
/// </summary>
sealed class Race
{
/// <summary>
/// The parameters for the TCP race competition.
/// </summary>
public TCPRaceParameters Parameters;
/// <summary>
/// The index of the next address to connect to.
/// </summary>
public int NextAddressIndex;
/// <summary>
/// The number of running lanes in the competition.
/// </summary>
public int RunningLanes;
public override string ToString() => $"[{nameof(Race)} {this.Parameters}, {this.NextAddressIndex}, {this.RunningLanes}]";
}
/// <summary>
/// Represents a racing lane in a TCP race competition.
/// </summary>
sealed class RacingLane
{
/// <summary>
/// The associated race and its parameters.
/// </summary>
public Race Race;
/// <summary>
/// The index of the address to connect to.
/// </summary>
public int AddressIndex;
/// <summary>
/// The index of the racing lane.
/// </summary>
public int LaneIndex;
/// <summary>
/// The socket used for the racing lane.
/// </summary>
public Socket Socket;
public override string ToString() => $"[{nameof(RacingLane)} {this.Race}, {this.AddressIndex}, {this.LaneIndex}, {this.Socket}]";
}
/// <summary>
/// Contains parameters and settings for a TCP race competition to establish connections.
/// </summary>
public sealed class TCPRaceParameters
{
/// <summary>
/// An array of DNS IP addresses to be used for racing to establish a connection.
/// </summary>
public DNSIPAddress[] Addresses;
/// <summary>
/// The hostname to connect to.
/// </summary>
public string Hostname;
/// <summary>
/// The port to connect to.
/// </summary>
public int Port;
/// <summary>
/// The cancellation token used to cancel the TCP race competition.
/// </summary>
public CancellationToken Token;
/// <summary>
/// A callback function to announce the winner of the TCP race competition.
/// </summary>
/// <param name="parameters">The TCPRaceParameters used for the race competition.</param>
/// <param name="result">The result of the race competition, including the winning socket or an error.</param>
public Action<TCPRaceParameters, TCPRaceResult> AnnounceWinnerCallback;
/// <summary>
/// Optional context for logging and tracking purposes.
/// </summary>
public LoggingContext Context;
/// <summary>
/// A user-defined tag associated with the TCP race parameters.
/// </summary>
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}]";
}
}
/// <summary>
/// <para>TCPRingmaster provides a method called <see cref="TCPRingmaster.StartCompetion(TCPRaceParameters)"/>, 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.</para>
/// The class allows to specify a callback function (through <see cref="TCPRaceParameters.AnnounceWinnerCallback"/>) 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.
/// <para>Additionally it includes logic for optimizing the order in which connection attempts are made:
/// <list type="bullet">
/// <item><description>It can shuffle the order of addresses to improve the chances of quickly finding a working address.</description></item>
/// <item><description>It handles scenarios where some addresses may not be working and prioritizes working addresses.</description></item>
/// </list>
/// </para>
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
public static class TCPRingmaster
{
/// <summary>
/// Starts a TCP race competition to establish connections based on the provided parameters.
/// </summary>
/// <param name="parameters">The parameters and settings for the TCP race competition.</param>
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);
}
}
/// <summary>
/// Inplace shuffles addresses.
/// </summary>
/// <param name="addresses">The array of <see cref="DNSIPAddress"/> to shuffle.</param>
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<Action<TCPRaceParameters, TCPRaceResult>>(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