#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