449 lines
20 KiB
C#
449 lines
20 KiB
C#
#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
|