mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Happy Eyeballs for HttpClient use.
All HttpClient usages in the engine now use Happy Eyeballs, same implementation as the launcher. Makes a IHttpClientHolder type so content can profit from this technology too. Didn't make use of this in all HttpClient usages in the engine itself, due to varying circumstances making it annoying to refactor.
This commit is contained in:
@@ -39,7 +39,7 @@ END TEMPLATE-->
|
||||
|
||||
### New features
|
||||
|
||||
*None yet*
|
||||
* `IHttpClientHolder` holds a shared `HttpClient` for use by content. It has Happy Eyeballs fixed and an appropriate `User-Agent`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -47,7 +47,7 @@ END TEMPLATE-->
|
||||
|
||||
### Other
|
||||
|
||||
*None yet*
|
||||
* Outgoing HTTP requests now all use Happy Eyeballs to try to prioritize IPv6. This is necessary because .NET still does not support this critical feature itself.
|
||||
|
||||
### Internal
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -71,12 +72,12 @@ internal sealed class HubManager
|
||||
_interval = TimeSpan.FromSeconds(interval);
|
||||
_httpClient?.Dispose();
|
||||
|
||||
_httpClient = new HttpClient(new SocketsHttpHandler
|
||||
{
|
||||
// Keep-alive connections stay open for longer than the advertise interval.
|
||||
// This way the same HTTPS connection can be re-used.
|
||||
PooledConnectionIdleTimeout = _interval + TimeSpan.FromSeconds(10),
|
||||
});
|
||||
var socketsHandler = HappyEyeballsHttp.CreateHttpHandler();
|
||||
// Keep-alive connections stay open for longer than the advertise interval.
|
||||
// This way the same HTTPS connection can be re-used.
|
||||
socketsHandler.PooledConnectionIdleTimeout = _interval + TimeSpan.FromSeconds(10);
|
||||
|
||||
_httpClient = new HttpClient(socketsHandler);
|
||||
|
||||
HttpClientUserAgent.AddUserAgent(_httpClient);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace Robust.Server
|
||||
deps.Register<INetConfigurationManagerInternal, ServerNetConfigurationManager>();
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IHttpClientHolder, HttpClientHolder>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -28,7 +29,7 @@ namespace Robust.Server.ServerStatus
|
||||
|
||||
// Ping watchdog every 15 seconds.
|
||||
private static readonly TimeSpan PingGap = TimeSpan.FromSeconds(15);
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly HttpClient _httpClient = new(HappyEyeballsHttp.CreateHttpHandler());
|
||||
|
||||
private TimeSpan? _lastPing;
|
||||
private string? _watchdogToken;
|
||||
|
||||
103
Robust.Shared/Network/HappyEyeballsHttp.cs
Normal file
103
Robust.Shared/Network/HappyEyeballsHttp.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Robust.Shared.Network;
|
||||
|
||||
internal static class HappyEyeballsHttp
|
||||
{
|
||||
// .NET does not implement Happy Eyeballs at the time of writing.
|
||||
// https://github.com/space-wizards/SS14.Launcher/issues/38
|
||||
// This is the workaround.
|
||||
//
|
||||
// Implementation taken from https://github.com/ppy/osu-framework/pull/4191/files
|
||||
|
||||
public static SocketsHttpHandler CreateHttpHandler()
|
||||
{
|
||||
return new SocketsHttpHandler
|
||||
{
|
||||
ConnectCallback = OnConnect,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether IPv6 should be preferred. Value may change based on runtime failures.
|
||||
/// </summary>
|
||||
private static bool _useIPv6 = Socket.OSSupportsIPv6;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the initial IPv6 check has been performed (to determine whether v6 is available or not).
|
||||
/// </summary>
|
||||
private static bool _hasResolvedIPv6Availability;
|
||||
|
||||
private const int FirstTryTimeout = 2000;
|
||||
|
||||
private static async ValueTask<Stream> OnConnect(
|
||||
SocketsHttpConnectionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_useIPv6)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localToken = cancellationToken;
|
||||
|
||||
if (!_hasResolvedIPv6Availability)
|
||||
{
|
||||
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
|
||||
var quickFailCts = new CancellationTokenSource(FirstTryTimeout);
|
||||
var linkedTokenSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
|
||||
|
||||
localToken = linkedTokenSource.Token;
|
||||
}
|
||||
|
||||
return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
_useIPv6 = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_hasResolvedIPv6Availability = true;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to IPv4.
|
||||
return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> AttemptConnection(
|
||||
AddressFamily addressFamily,
|
||||
SocketsHttpConnectionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
|
||||
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
|
||||
NoDelay = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
// The stream should take the ownership of the underlying socket,
|
||||
// closing it when it's disposed.
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Robust.Shared/Network/HttpClientHolder.cs
Normal file
39
Robust.Shared/Network/HttpClientHolder.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Net.Http;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Holds a shared <see cref="HttpClient"/> for the whole program to use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The shared <see cref="HttpClient"/> has an appropriate <c>User-Agent</c> set for Robust,
|
||||
/// and correctly supports Happy Eyeballs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This interface is not available on the client.
|
||||
/// Engine code may use <see cref="HttpClientHolder"/> directly instead,
|
||||
/// content code can't send arbitrary HTTP requests.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IHttpClientHolder
|
||||
{
|
||||
HttpClient Client { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IHttpClientHolder"/>.
|
||||
/// </summary>
|
||||
internal sealed class HttpClientHolder : IHttpClientHolder
|
||||
{
|
||||
public HttpClient Client { get; }
|
||||
|
||||
public HttpClientHolder()
|
||||
{
|
||||
Client = new HttpClient(HappyEyeballsHttp.CreateHttpHandler());
|
||||
HttpClientUserAgent.AddUserAgent(Client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace Robust.Shared.Network
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, authServer + "api/session/join");
|
||||
request.Content = JsonContent.Create(joinReq);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", authToken);
|
||||
var joinResp = await _httpClient.SendAsync(request, cancel);
|
||||
var joinResp = await _http.Client.SendAsync(request, cancel);
|
||||
|
||||
joinResp.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace Robust.Shared.Network
|
||||
var authHash = Base64Helpers.ConvertToBase64Url(authHashBytes);
|
||||
|
||||
var url = $"{authServer}api/session/hasJoined?hash={authHash}&userId={msgEncResponse.UserId}";
|
||||
var joinedRespJson = await _httpClient.GetFromJsonAsync<HasJoinedResponse>(url);
|
||||
var joinedRespJson = await _http.Client.GetFromJsonAsync<HasJoinedResponse>(url);
|
||||
|
||||
if (joinedRespJson is not {IsValid: true})
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
@@ -110,6 +109,7 @@ namespace Robust.Shared.Network
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
[Dependency] private readonly HttpClientHolder _http = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Holds lookup table for NetMessage.Id -> NetMessage.Type
|
||||
@@ -134,8 +134,6 @@ namespace Robust.Shared.Network
|
||||
|
||||
private readonly HashSet<NetUserId> _awaitingDisconnectToConnect = new HashSet<NetUserId>();
|
||||
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
private ISawmill _logger = default!;
|
||||
private ISawmill _authLogger = default!;
|
||||
|
||||
@@ -247,8 +245,6 @@ namespace Robust.Shared.Network
|
||||
|
||||
_strings.Sawmill = _logger;
|
||||
|
||||
HttpClientUserAgent.AddUserAgent(_httpClient);
|
||||
|
||||
SynchronizeNetTime();
|
||||
|
||||
IsServer = isServer;
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Robust.Shared
|
||||
deps.Register<IVerticesSimplifier, RamerDouglasPeuckerSimplifier>();
|
||||
deps.Register<IParallelManager, ParallelManager>();
|
||||
deps.Register<IParallelManagerInternal, ParallelManager>();
|
||||
deps.Register<HttpClientHolder>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user