.Netcore HttpClient源码探究
源码搜索与概述
搜索HttpClient源码 https://source.dot.net/#System.Net.Http/System/Net/Http/HttpClient.cs
1、HttpClient 依赖HttpClientHandler或HttpMessageHandler,HttpClientHandler也继承自HttpMessageHandler
2、HttpClientHandler依赖 SocketsHttpHandler,SocketsHttpHandler继承HttpMessageHandler,并支持跨平台
3、SocketsHttpHandler依赖HttpConnectionHandler或HttpAuthenticatedConnectionHandler,
这两个又依赖HttpConnectionPoolManager
4、HttpConnectionPoolManager维护ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>,调用HttpConnectionPool进行Send
5、HttpConnectionPool再根据Http3/http2或其他不同的配置再进行细分到不同的流去处理。
查看示例
1.可以看到HttpClient继承HttpMessageInvoker,并提供了三个不同参数的构造函数
public partial class HttpClient : HttpMessageInvoker
public HttpClient() : this(new HttpClientHandler())
{
}
public HttpClient(HttpMessageHandler handler) : this(handler, true)
{
}
public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler)
{
_timeout = s_defaultTimeout;
_maxResponseContentBufferSize = HttpContent.MaxBufferSize;
_pendingRequestsCts = new CancellationTokenSource();
}
首先跟踪第一个构造函数:public HttpClient() : this(new HttpClientHandler())
using HttpHandlerType = System.Net.Http.SocketsHttpHandler;
namespace System.Net.Http
{
public partial class HttpClientHandler : HttpMessageHandler
{
private readonly HttpHandlerType _underlyingHandler;
private HttpMessageHandler Handler
#if TARGET_BROWSER
{ get; }
#else
=> _underlyingHandler;
#endif
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
Handler.Send(request, cancellationToken);
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Handler.SendAsync(request, cancellationToken);
可知依赖 HttpHandlerType=SocketsHttpHandler
[UnsupportedOSPlatform("browser")]
public sealed class SocketsHttpHandler : HttpMessageHandler
{
private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();
private HttpMessageHandlerStage? _handler;
private bool _disposed;
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(SocketsHttpHandler));
}
}
private void CheckDisposedOrStarted()
{
CheckDisposed();
if (_handler != null)
{
throw new InvalidOperationException(SR.net_http_operation_started);
}
}
关注SendAsync
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest);
}
CheckDisposed();
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<HttpResponseMessage>(cancellationToken);
}
HttpMessageHandler handler = _handler ?? SetupHandlerChain();
Exception? error = ValidateAndNormalizeRequest(request);
if (error != null)
{
return Task.FromException<HttpResponseMessage>(error);
}
return handler.SendAsync(request, cancellationToken);
}
//设置初始化handler:SetupHandlerChain
private HttpMessageHandlerStage SetupHandlerChain()
{
// Clone the settings to get a relatively consistent view that won't change after this point.
// (This isn't entirely complete, as some of the collections it contains aren't currently deeply cloned.)
HttpConnectionSettings settings = _settings.CloneAndNormalize();
HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings);
HttpMessageHandlerStage handler;
if (settings._credentials == null)
{
handler = new HttpConnectionHandler(poolManager);
}
else
{
handler = new HttpAuthenticatedConnectionHandler(poolManager);
}
// DiagnosticsHandler is inserted before RedirectHandler so that trace propagation is done on redirects as well
if (DiagnosticsHandler.IsGloballyEnabled() && settings._activityHeadersPropagator is DistributedContextPropagator propagator)
{
handler = new DiagnosticsHandler(handler, propagator, settings._allowAutoRedirect);
}
if (settings._allowAutoRedirect)
{
// Just as with WinHttpHandler, for security reasons, we do not support authentication on redirects
// if the credential is anything other than a CredentialCache.
// We allow credentials in a CredentialCache since they are specifically tied to URIs.
HttpMessageHandlerStage redirectHandler =
(settings._credentials == null || settings._credentials is CredentialCache) ?
handler :
new HttpConnectionHandler(poolManager); // will not authenticate
handler = new RedirectHandler(settings._maxAutomaticRedirections, handler, redirectHandler);
}
if (settings._automaticDecompression != DecompressionMethods.None)
{
handler = new DecompressionHandler(settings._automaticDecompression, handler);
}
// Ensure a single handler is used for all requests.
if (Interlocked.CompareExchange(ref _handler, handler, null) != null)
{
handler.Dispose();
}
return _handler;
}
从上面可以看到根据HttpConnectionSettings._credentials 的配置进行不同的初始化
HttpConnectionSettings settings = _settings.CloneAndNormalize();
HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings);
HttpMessageHandlerStage handler;
if (settings._credentials == null)
{
handler = new HttpConnectionHandler(poolManager);
}
else
{
handler = new HttpAuthenticatedConnectionHandler(poolManager);
}
关注HttpConnectionHandler和HttpAuthenticatedConnectionHandler,均依赖HttpConnectionPoolManager
internal sealed class HttpConnectionHandler : HttpMessageHandlerStage
{
private readonly HttpConnectionPoolManager _poolManager;
public HttpConnectionHandler(HttpConnectionPoolManager poolManager)
{
_poolManager = poolManager;
}
internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
return _poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_poolManager.Dispose();
}
base.Dispose(disposing);
}
}
可见HttpConnectionPoolManager 维护了一个链接池。
public HttpConnectionPoolManager(HttpConnectionSettings settings)
{
_settings = settings;
_pools = new ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>();
管理链接的部分:
public ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken)
{
HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);
HttpConnectionPool? pool;
while (!_pools.TryGetValue(key, out pool))
{
pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri);
if (_cleaningTimer == null)
{
// There's no cleaning timer, which means we're not adding connections into pools, but we still need
// the pool object for this request. We don't need or want to add the pool to the pools, though,
// since we don't want it to sit there forever, which it would without the cleaning timer.
break;
}
if (_pools.TryAdd(key, pool))
{
// We need to ensure the cleanup timer is running if it isn't
// already now that we added a new connection pool.
lock (SyncObj)
{
if (!_timerIsRunning)
{
SetCleaningTimer(_cleanPoolTimeout);
}
}
break;
}
// We created a pool and tried to add it to our pools, but some other thread got there before us.
// We don't need to Dispose the pool, as that's only needed when it contains connections
// that need to be closed.
}
return pool.SendAsync(request, async, doRequestAuth, cancellationToken);
}
可以看到由HttpConnectionPool发起SendAsync调用
https://source.dot.net/#System.Net.Http/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs,155362accc97d7ca
public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri)
{
_poolManager = poolManager;
_kind = kind;
_proxyUri = proxyUri;
_maxHttp11Connections = Settings._maxConnectionsPerServer;
if (host != null)
{
_originAuthority = new HttpAuthority(host, port);
}
_http2Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version20;
if (IsHttp3Supported())
{
_http3Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version30 && (_poolManager.Settings._quicImplementationProvider ?? QuicImplementationProviders.Default).IsSupported;
}
switch (kind)
{
case HttpConnectionKind.Http:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri == null);
_http3Enabled = false;
break;
case HttpConnectionKind.Https:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(sslHostName != null);
Debug.Assert(proxyUri == null);
break;
case HttpConnectionKind.Proxy:
Debug.Assert(host == null);
Debug.Assert(port == 0);
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri != null);
_http2Enabled = false;
_http3Enabled = false;
break;
case HttpConnectionKind.ProxyTunnel:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri != null);
_http2Enabled = false;
_http3Enabled = false;
break;
case HttpConnectionKind.SslProxyTunnel:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(sslHostName != null);
Debug.Assert(proxyUri != null);
_http3Enabled = false; // TODO: how do we tunnel HTTP3?
break;
case HttpConnectionKind.ProxyConnect:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri != null);
// Don't enforce the max connections limit on proxy tunnels; this would mean that connections to different origin servers
// would compete for the same limited number of connections.
// We will still enforce this limit on the user of the tunnel (i.e. ProxyTunnel or SslProxyTunnel).
_maxHttp11Connections = int.MaxValue;
_http2Enabled = false;
_http3Enabled = false;
break;
case HttpConnectionKind.SocksTunnel:
case HttpConnectionKind.SslSocksTunnel:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(proxyUri != null);
_http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3
break;
default:
Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor");
break;
}
if (!_http3Enabled)
{
// Avoid parsing Alt-Svc headers if they won't be used.
_altSvcEnabled = false;
}
string? hostHeader = null;
if (_originAuthority != null)
{
// Precalculate ASCII bytes for Host header
// Note that if _host is null, this is a (non-tunneled) proxy connection, and we can't cache the hostname.
hostHeader =
(_originAuthority.Port != (sslHostName == null ? DefaultHttpPort : DefaultHttpsPort)) ?
$"{_originAuthority.IdnHost}:{_originAuthority.Port}" :
_originAuthority.IdnHost;
// Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
_hostHeaderValueBytes = Encoding.ASCII.GetBytes(hostHeader);
Debug.Assert(Encoding.ASCII.GetString(_hostHeaderValueBytes) == hostHeader);
if (sslHostName == null)
{
_http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader);
_http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader);
}
}
if (sslHostName != null)
{
_sslOptionsHttp11 = ConstructSslOptions(poolManager, sslHostName);
_sslOptionsHttp11.ApplicationProtocols = null;
if (_http2Enabled)
{
_sslOptionsHttp2 = ConstructSslOptions(poolManager, sslHostName);
_sslOptionsHttp2.ApplicationProtocols = s_http2ApplicationProtocols;
_sslOptionsHttp2Only = ConstructSslOptions(poolManager, sslHostName);
_sslOptionsHttp2Only.ApplicationProtocols = s_http2OnlyApplicationProtocols;
// Note:
// The HTTP/2 specification states:
// "A deployment of HTTP/2 over TLS 1.2 MUST disable renegotiation.
// An endpoint MUST treat a TLS renegotiation as a connection error (Section 5.4.1)
// of type PROTOCOL_ERROR."
// which suggests we should do:
// _sslOptionsHttp2.AllowRenegotiation = false;
// However, if AllowRenegotiation is set to false, that will also prevent
// renegotation if the server denies the HTTP/2 request and causes a
// downgrade to HTTP/1.1, and the current APIs don't provide a mechanism
// by which AllowRenegotiation could be set back to true in that case.
// For now, if an HTTP/2 server erroneously issues a renegotiation, we'll
// allow it.
Debug.Assert(hostHeader != null);
_http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader);
_http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader);
}
if (IsHttp3Supported())
{
if (_http3Enabled)
{
_sslOptionsHttp3 = ConstructSslOptions(poolManager, sslHostName);
_sslOptionsHttp3.ApplicationProtocols = s_http3ApplicationProtocols;
}
}
}
// Set up for PreAuthenticate. Access to this cache is guarded by a lock on the cache itself.
if (_poolManager.Settings._preAuthenticate)
{
PreAuthCredentials = new CredentialCache();
}
if (NetEventSource.Log.IsEnabled()) Trace($"{this}");
}
发起调用
public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
if (doRequestAuth && Settings._credentials != null)
{
return AuthenticationHelper.SendWithRequestAuthAsync(request, async, Settings._credentials, Settings._preAuthenticate, this, cancellationToken);
}
return SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
}
public ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
if (DoProxyAuth && ProxyCredentials is not null)
{
return AuthenticationHelper.SendWithProxyAuthAsync(request, _proxyUri!, async, ProxyCredentials, doRequestAuth, this, cancellationToken);
}
return SendWithRetryAsync(request, async, doRequestAuth, cancellationToken);
}
public async ValueTask<HttpResponseMessage> SendWithRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
int retryCount = 0;
while (true)
{
// Loop on connection failures (or other problems like version downgrade) and retry if possible.
try
{
return await SendAndProcessAltSvcAsync(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnConnectionFailure)
{
Debug.Assert(retryCount >= 0 && retryCount <= MaxConnectionFailureRetries);
if (retryCount == MaxConnectionFailureRetries)
{
if (NetEventSource.Log.IsEnabled())
{
Trace($"MaxConnectionFailureRetries limit of {MaxConnectionFailureRetries} hit. Retryable request will not be retried. Exception: {e}");
}
throw;
}
retryCount++;
if (NetEventSource.Log.IsEnabled())
{
Trace($"Retry attempt {retryCount} after connection failure. Connection exception: {e}");
}
// Eat exception and try again.
}
catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnLowerHttpVersion)
{
// Throw if fallback is not allowed by the version policy.
if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e);
}
if (NetEventSource.Log.IsEnabled())
{
Trace($"Retrying request because server requested version fallback: {e}");
}
// Eat exception and try again on a lower protocol version.
request.Version = HttpVersion.Version11;
}
catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnStreamLimitReached)
{
if (NetEventSource.Log.IsEnabled())
{
Trace($"Retrying request on another HTTP/2 connection after active streams limit is reached on existing one: {e}");
}
// Eat exception and try again.
}
}
}
private async ValueTask<HttpResponseMessage> SendAndProcessAltSvcAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
HttpResponseMessage response = await DetermineVersionAndSendAsync(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
// Check for the Alt-Svc header, to upgrade to HTTP/3.
if (_altSvcEnabled && response.Headers.TryGetValues(KnownHeaders.AltSvc.Descriptor, out IEnumerable<string>? altSvcHeaderValues))
{
HandleAltSvc(altSvcHeaderValues, response.Headers.Age);
}
return response;
}
private async ValueTask<HttpResponseMessage> DetermineVersionAndSendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
HttpResponseMessage? response;
if (IsHttp3Supported())
{
response = await TrySendUsingHttp3Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
if (response is not null)
{
return response;
}
}
// We cannot use HTTP/3. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
throw GetVersionException(request, 3);
}
response = await TrySendUsingHttp2Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
if (response is not null)
{
return response;
}
// We cannot use HTTP/2. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
throw GetVersionException(request, 2);
}
return await SendUsingHttp11Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
}
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private async ValueTask<Http3Connection> GetHttp3ConnectionAsync(HttpRequestMessage request, HttpAuthority authority, CancellationToken cancellationToken)
{
Debug.Assert(_kind == HttpConnectionKind.Https);
Debug.Assert(_http3Enabled == true);
Http3Connection? http3Connection = Volatile.Read(ref _http3Connection);
if (http3Connection != null)
{
if (CheckExpirationOnGet(http3Connection) || http3Connection.Authority != authority)
{
// Connection expired.
if (NetEventSource.Log.IsEnabled()) http3Connection.Trace("Found expired HTTP3 connection.");
http3Connection.Dispose();
InvalidateHttp3Connection(http3Connection);
}
else
{
// Connection exists and it is still good to use.
if (NetEventSource.Log.IsEnabled()) Trace("Using existing HTTP3 connection.");
_usedSinceLastCleanup = true;
return http3Connection;
}
}
// Ensure that the connection creation semaphore is created
if (_http3ConnectionCreateLock == null)
{
lock (SyncObj)
{
if (_http3ConnectionCreateLock == null)
{
_http3ConnectionCreateLock = new SemaphoreSlim(1);
}
}
}
await _http3ConnectionCreateLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_http3Connection != null)
{
// Someone beat us to creating the connection.
if (NetEventSource.Log.IsEnabled())
{
Trace("Using existing HTTP3 connection.");
}
return _http3Connection;
}
if (NetEventSource.Log.IsEnabled())
{
Trace("Attempting new HTTP3 connection.");
}
QuicConnection quicConnection;
try
{
quicConnection = await ConnectHelper.ConnectQuicAsync(request, Settings._quicImplementationProvider ?? QuicImplementationProviders.Default, new DnsEndPoint(authority.IdnHost, authority.Port), _sslOptionsHttp3!, cancellationToken).ConfigureAwait(false);
}
catch
{
// Disables HTTP/3 until server announces it can handle it via Alt-Svc.
BlocklistAuthority(authority);
throw;
}
//TODO: NegotiatedApplicationProtocol not yet implemented.
#if false
if (quicConnection.NegotiatedApplicationProtocol != SslApplicationProtocol.Http3)
{
BlocklistAuthority(authority);
throw new HttpRequestException("QUIC connected but no HTTP/3 indicated via ALPN.", null, RequestRetryType.RetryOnSameOrNextProxy);
}
#endif
http3Connection = new Http3Connection(this, _originAuthority, authority, quicConnection);
_http3Connection = http3Connection;
if (NetEventSource.Log.IsEnabled())
{
Trace("New HTTP3 connection established.");
}
return http3Connection;
}
finally
{
_http3ConnectionCreateLock.Release();
}
}