- Initial commit
This commit is contained in:
448
Runtime/Shared/PlatformSupport/Network/Tcp/TCPRingmaster.cs
Normal file
448
Runtime/Shared/PlatformSupport/Network/Tcp/TCPRingmaster.cs
Normal 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
|
||||
Reference in New Issue
Block a user