¿Deberíamos crear una nueva instancia única de HttpClient para todas las solicitudes?

58

Hace poco me encontré con esta publicación de blog de monstruos asp.net que habla sobre problemas con el uso HttpClientde la siguiente manera:

using(var client = new HttpClient())
{
}

Según la publicación del blog, si desechamos el HttpClientdespués de cada solicitud, puede mantener abiertas las conexiones TCP. Esto puede conducir potencialmente a System.Net.Sockets.SocketException.

La forma correcta según la publicación es crear una sola instancia, HttpClientya que ayuda a reducir el desperdicio de enchufes.

De la publicación:

Si compartimos una sola instancia de HttpClient, podemos reducir el desperdicio de sockets reutilizándolos:

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

Siempre he dispuesto el HttpClientobjeto después de usarlo, ya que considero que esta es la mejor manera de usarlo. Pero esta publicación de blog ahora me hace sentir que lo estaba haciendo mal todo este tiempo.

¿Deberíamos crear una nueva instancia única HttpClientpara todas las solicitudes? ¿Hay alguna dificultad al usar la instancia estática?

Ankit Vijay
fuente
¿Ha encontrado algún problema que haya atribuido a la forma en que lo está usando?
whatsisname
Tal vez verifique esta respuesta y también esto .
John Wu
@whatsisname no, no lo he hecho, pero al mirar el blog, sentí que podría estar usando esto mal todo el tiempo Por lo tanto, quería entender de otros desarrolladores si ven algún problema en cualquiera de los enfoques.
Ankit Vijay
3
No lo he probado yo mismo (por lo que no proporciono esto como respuesta), pero de acuerdo con Microsoft a partir de .NET Core 2.1 se supone que debes usar HttpClientFactory como se describe en docs.microsoft.com/en-us/dotnet/standard/ ...
Joeri Sebrechts
(Como se indicó en mi respuesta, solo quería hacerlo más visible, por lo que estoy escribiendo un breve comentario). La instancia estática manejará correctamente la conexión tcp al cerrar el apretón de manos, una vez que haga Close()o inicie un nuevo Get(). Si acaba de deshacerse del cliente cuando haya terminado con él, no habrá nadie para manejar ese apretón de manos de cierre y todos sus puertos tendrán el estado TIME_WAIT, por eso.
Mladen B.

Respuestas:

40

Parece una publicación de blog convincente. Sin embargo, antes de tomar una decisión, primero realizaría las mismas pruebas que realizó el escritor del blog, pero en su propio código. También intentaría descubrir un poco más sobre HttpClient y su comportamiento.

Esta publicación dice:

Una instancia de HttpClient es una colección de configuraciones aplicadas a todas las solicitudes ejecutadas por esa instancia. Además, cada instancia de HttpClient usa su propio grupo de conexiones, aislando sus solicitudes de las solicitudes ejecutadas por otras instancias de HttpClient.

Entonces, lo que probablemente sucede cuando se comparte un HttpClient es que las conexiones se están reutilizando, lo cual está bien si no necesita conexiones persistentes. La única forma de saber con certeza si esto es importante para su situación es ejecutar sus propias pruebas de rendimiento.

Si cava, encontrará varios otros recursos que abordan este problema (incluido un artículo sobre las mejores prácticas de Microsoft), por lo que probablemente sea una buena idea implementar de todos modos (con algunas precauciones).

Referencias

¿Está utilizando Httpclient incorrecto y está desestabilizando su software
Singleton HttpClient? Tenga cuidado con este comportamiento grave y cómo solucionarlo
Patrones y prácticas de Microsoft - Optimización del rendimiento: instanciación incorrecta Instancia
única de HttpClient reutilizable en la revisión de código
Singleton HttpClient no respeta los cambios de DNS (CoreFX)
Consejos generales para usar HttpClient

Robert Harvey
fuente
1
Esa es una buena lista extensa. Esta es mi lectura de fin de semana.
Ankit Vijay
"Si cava, encontrará varios otros recursos que abordan este problema ..." ¿quiere decir que el problema de conexión TCP abierta?
Ankit Vijay
Respuesta corta: use un HttpClient estático . Si necesita admitir cambios de DNS (de su servidor web u otros servidores), debe preocuparse por la configuración del tiempo de espera.
Jess
3
Es un testimonio de lo desordenado que HttpClient es que usarlo es una "lectura de fin de semana" como comentó @AnkitVijay.
usr
@Jess además de los cambios de DNS: ¿arrojar todo el tráfico de su cliente a través de un solo socket también dañará el equilibrio de carga?
Iain
16

Llego tarde a la fiesta, pero aquí está mi viaje de aprendizaje sobre este tema complicado.

1. ¿Dónde podemos encontrar al defensor oficial sobre la reutilización de HttpClient?

Quiero decir, si se pretende reutilizar HttpClient y hacerlo es importante , dicho defensor está mejor documentado en su propia documentación API, en lugar de estar oculto en muchos "Temas avanzados", "Patrón de rendimiento (anti)" u otras publicaciones de blog. . De lo contrario, ¿cómo se supone que un nuevo alumno debe saberlo antes de que sea demasiado tarde?

A partir de ahora (mayo de 2018), el primer resultado de búsqueda al buscar en Google "c # httpclient" apunta a esta página de referencia de API en MSDN , que no menciona esa intención en absoluto. Bueno, la lección 1 aquí para los novatos es, siempre haga clic en el enlace "Otras versiones" justo después del título de la página de ayuda de MSDN, probablemente encontrará enlaces a la "versión actual" allí. En este caso de HttpClient, lo llevará al último documento que contiene esa descripción de intención .

Sospecho que muchos desarrolladores que eran nuevos en este tema tampoco encontraron la página de documentación correcta, es por eso que este conocimiento no está muy extendido, y la gente se sorprendió cuando lo descubrieron más tarde , posiblemente de una manera difícil .

2. La concepción (¿equivocada?) De using IDisposable

Este es un poco fuera de tema, pero aún vale la pena señalar que, no es una coincidencia ver a las personas en esas publicaciones de blog mencionadas culpando cómo HttpClientla IDisposableinterfaz hace que tienden a usar el using (var client = new HttpClient()) {...}patrón y luego conducen al problema.

Creo que eso se reduce a una concepción tácita (¿equivocada?): "Se espera que un objeto IDisposable sea de corta duración" .

SIN EMBARGO, aunque ciertamente parece algo de corta duración cuando escribimos código en este estilo:

using (var foo = new SomeDisposableObject())
{
    ...
}

La documentación oficial sobre IDisposable nunca menciona que los IDisposableobjetos tienen que ser de corta duración. Por definición, IDisposable es simplemente un mecanismo para permitirle liberar recursos no administrados. Nada mas. En ese sentido, se ESPERA que eventualmente active la eliminación, pero no requiere que lo haga de manera efímera.

Por lo tanto, es su trabajo elegir adecuadamente cuándo activar la eliminación, basándose en el requisito del ciclo de vida de su objeto real. No hay nada que le impida usar un ID desechable de una manera duradera:

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

Con esta nueva comprensión, ahora que volvemos a visitar esa publicación de blog , podemos notar claramente que la "corrección" se inicializa HttpClientuna vez, pero nunca la elimina, por eso podemos ver en su salida de netstat que, la conexión permanece en estado ESTABLECIDO, lo que significa que tiene NO se ha cerrado correctamente. Si estuviera cerrado, su estado estaría en TIME_WAIT en su lugar. En la práctica, no es un gran problema filtrar solo una conexión abierta después de que termine todo el programa, y ​​el póster del blog aún ve una ganancia de rendimiento después de la solución; pero aún así, es conceptualmente incorrecto culpar a IDisposable y elegir NO desecharlo.

3. ¿Tenemos que poner HttpClient en una propiedad estática, o incluso ponerlo como un singleton?

Basado en la comprensión de la sección anterior, creo que la respuesta aquí se vuelve clara: "no necesariamente". Realmente depende de cómo organice su código, siempre que reutilice un HttpClient Y (idealmente) lo elimine eventualmente.

Hilarantemente, ni siquiera el ejemplo en la sección de Comentarios del documento oficial actual lo hace estrictamente correcto. Define una clase "GoodController", que contiene una propiedad HttpClient estática que no se eliminará; que desobedece lo que otro ejemplo en la sección de Ejemplos enfatiza: "necesita llamar a disponer ... para que la aplicación no pierda recursos".

Y, por último, el singleton no está exento de desafíos propios.

"¿Cuántas personas piensan que la variable global es una buena idea? Nadie.

¿Cuántas personas piensan que Singleton es una buena idea? Unos pocos.

¿Lo que da? Los singletons son solo un montón de variables globales ".

- Citado de esta charla inspiradora, "Global State and Singletons"

PS: SqlConnection

Este es irrelevante para las preguntas y respuestas actuales, pero probablemente sea bueno saberlo. El patrón de uso de SqlConnection es diferente. Usted NO tiene que volver a utilizar SqlConnection , ya que se encargará de su grupo de conexión mejor de esa manera.

La diferencia es causada por su enfoque de implementación. Cada instancia de HttpClient usa su propio grupo de conexiones (citado desde aquí ); pero SqlConnection en sí es administrado por un grupo de conexiones central, de acuerdo con esto .

Y aún debe deshacerse de SqlConnection, igual que se supone que debe hacer para HttpClient.

RayLuo
fuente
14

Hice algunas pruebas para ver mejoras de rendimiento con estática HttpClient. Usé el siguiente código para mis pruebas:

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

Para las pruebas:

  • Ejecuté el código con 10, 100, 1000 y 1000 conexiones.
  • Corrió cada prueba 3 veces para averiguar el promedio.
  • Ejecuté un método a la vez

Encontré la mejora del rendimiento entre el 40% y el 60% usando estática en HttpClientlugar de desecharla para HttpClientsolicitarla. He puesto los detalles del resultado de la prueba de rendimiento en la publicación del blog aquí .

Ankit Vijay
fuente
1

Para cerrar correctamente la conexión TCP , debemos completar una secuencia de paquetes FIN - FIN + ACK - ACK (al igual que SYN - SYN + ACK - ACK, al abrir una conexión TCP ). Si solo llamamos a un método .Close () (generalmente ocurre cuando un HttpClient está desechando), y no esperamos que el lado remoto confirme nuestra solicitud de cierre (con FIN + ACK), terminamos con el estado TIME_WAIT en el puerto TCP local, porque desechamos nuestro oyente (HttpClient) y nunca tuvimos la oportunidad de restablecer el estado del puerto a un estado cerrado adecuado, una vez que el par remoto nos envía el paquete FIN + ACK.

La forma correcta de cerrar la conexión TCP sería llamar al método .Close () y esperar a que el evento de cierre del otro lado (FIN + ACK) llegue de nuestro lado. Solo entonces podemos enviar nuestro ACK final y eliminar el HttpClient.

Solo para agregar, tiene sentido mantener abiertas las conexiones TCP, si está realizando solicitudes HTTP, debido al encabezado HTTP "Conexión: Keep-Alive". Además, puede solicitar al par remoto que cierre la conexión por usted, en su lugar, estableciendo el encabezado HTTP "Conexión: Cerrar". De esa manera, sus puertos locales siempre estarán cerrados correctamente, en lugar de estar en un estado TIME_WAIT.

Mladen B.
fuente
1

Aquí hay un cliente API básico que utiliza HttpClient y HttpClientHandler de manera eficiente. Cuando crea un nuevo HttpClient para realizar una solicitud, hay muchos gastos generales. NO vuelva a crear HttpClient para cada solicitud. Reutilice HttpClient tanto como sea posible ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

Uso:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Alper Ebicoglu
fuente
-5

No hay una sola forma de usar la clase HttpClient. La clave es diseñar su aplicación de una manera que tenga sentido para su entorno y sus limitaciones.

HTTP es un gran protocolo para usar cuando necesita exponer API públicas. También se puede usar de manera efectiva para servicios internos de baja latencia y bajo peso, aunque el patrón de la cola de mensajes RPC suele ser una mejor opción para los servicios internos.

Hay mucha complejidad en hacer HTTP bien.

Considera lo siguiente:

  1. Crear un socket y establecer una conexión TCP utiliza el ancho de banda y el tiempo de la red.
  2. HTTP / 1.1 admite solicitudes de canalización en el mismo socket. Enviar múltiples solicitudes una tras otra, sin necesidad de esperar las respuestas anteriores, esto probablemente sea responsable de la mejora de la velocidad informada por la publicación del blog.
  3. Almacenamiento en caché y equilibrador de carga: si tiene un equilibrador de carga frente a los servidores, asegurarse de que sus solicitudes tengan encabezados de caché adecuados puede reducir la carga en sus servidores y obtener las respuestas a los clientes más rápido.
  4. No vuelvas a sondear un recurso, usa el agrupamiento HTTP para devolver respuestas periódicas.

Pero, sobre todo, pruebe, mida y confirme. Si no se comporta como se diseñó, entonces podemos responder preguntas específicas sobre cómo lograr los resultados esperados.

Michael Shaw
fuente
44
En realidad, esto no responde nada de lo que se pregunta.
whatsisname
Parece suponer que hay UNA forma correcta. No creo que la haya. Sé que debe usarlo de la manera apropiada, luego probar y medir cómo se comporta, y luego ajustar su enfoque hasta que esté satisfecho.
Michael Shaw
Escribió un poco sobre el uso de usar HTTP o no para comunicarse. El OP preguntó sobre la mejor manera de usar un componente de biblioteca en particular.
whatsisname
1
@MichaelShaw: HttpClientimplementos IDisposable. Por lo tanto, no es irrazonable esperar que sea un objeto de corta duración que sepa cómo limpiarse por sí mismo, adecuado para envolverlo en una usingdeclaración cada vez que lo necesite. Desafortunadamente, no es así como funciona realmente. La publicación de blog que el OP enlazó claramente demuestra que hay recursos (específicamente, conexiones de socket TCP) que viven mucho después de que la usingdeclaración haya salido del alcance y el HttpClientobjeto presumiblemente haya sido eliminado.
Robert Harvey
1
Entiendo ese proceso de pensamiento. Es solo si estaba pensando en HTTP desde el punto de vista de la arquitectura y tenía la intención de hacer muchas solicitudes al mismo servicio; entonces estaría pensando en el almacenamiento en caché y la canalización, y luego la idea de hacer de HttpClient un objeto de corta duración simplemente me siento mal. Del mismo modo, si realiza solicitudes a diferentes servidores y no obtendría ningún beneficio de mantener vivo el socket, entonces tiene sentido deshacerse del objeto HttpClient después de su uso.
Michael Shaw