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:
Pieter-Jan Briers
2023-07-31 22:51:16 +02:00
parent cb6645aebe
commit 1c7ae13bfa
10 changed files with 158 additions and 16 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -93,6 +93,7 @@ namespace Robust.Server
deps.Register<INetConfigurationManagerInternal, ServerNetConfigurationManager>();
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
deps.Register<NetworkResourceManager>();
deps.Register<IHttpClientHolder, HttpClientHolder>();
}
}
}

View File

@@ -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;

View 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;
}
}
}

View 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);
}
}

View File

@@ -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();

View File

@@ -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})
{

View File

@@ -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;

View File

@@ -46,6 +46,7 @@ namespace Robust.Shared
deps.Register<IVerticesSimplifier, RamerDouglasPeuckerSimplifier>();
deps.Register<IParallelManager, ParallelManager>();
deps.Register<IParallelManagerInternal, ParallelManager>();
deps.Register<HttpClientHolder>();
}
}
}