Decidir entre HttpClient y WebClient

218

Nuestra aplicación web se ejecuta en .Net Framework 4.0. La UI llama a los métodos del controlador a través de llamadas ajax.

Necesitamos consumir el servicio REST de nuestro proveedor. Estoy evaluando la mejor manera de llamar al servicio REST en .Net 4.0. El servicio REST requiere un esquema de autenticación básico y puede devolver datos tanto en XML como en JSON. No hay requisitos para cargar / descargar grandes datos y no veo nada en el futuro. Eché un vistazo a algunos proyectos de código fuente abierto para el consumo de REST y no encontré ningún valor en ellos para justificar una dependencia adicional en el proyecto. Comenzó a evaluar WebClienty HttpClient. Descargué HttpClient para .Net 4.0 de NuGet.

Busqué diferencias entre WebClienty, HttpClienty este sitio mencionó que solo HttpClient puede manejar llamadas concurrentes y puede reutilizar DNS resuelto, configuración de cookies y autenticación. Todavía tengo que ver los valores prácticos que podemos ganar debido a las diferencias.

Hice una prueba de rendimiento rápida para encontrar cómo funcionan WebClient(llamadas de sincronización), HttpClient(sincronización y asíncrono). Y aquí están los resultados:

Usar la misma HttpClientinstancia para todas las solicitudes (min - max)

WebClient sync: 8 ms - 167 ms
HttpClient sync: 3 ms - 7228 ms
HttpClient async: 985 - 10405 ms

Usando un nuevo HttpClientpara cada solicitud (min - max)

WebClient sync: 4 ms - 297 ms
HttpClient sync: 3 ms - 7953 ms
HttpClient async: 1027 - 10834 ms

Código

public class AHNData
{
    public int i;
    public string str;
}

public class Program
{
    public static HttpClient httpClient = new HttpClient();
    private static readonly string _url = "http://localhost:9000/api/values/";

    public static void Main(string[] args)
    {
       #region "Trace"
       Trace.Listeners.Clear();

       TextWriterTraceListener twtl = new TextWriterTraceListener(
           "C:\\Temp\\REST_Test.txt");
       twtl.Name = "TextLogger";
       twtl.TraceOutputOptions = TraceOptions.ThreadId | TraceOptions.DateTime;

       ConsoleTraceListener ctl = new ConsoleTraceListener(false);
       ctl.TraceOutputOptions = TraceOptions.DateTime;

       Trace.Listeners.Add(twtl);
       Trace.Listeners.Add(ctl);
       Trace.AutoFlush = true;
       #endregion

       int batchSize = 1000;

       ParallelOptions parallelOptions = new ParallelOptions();
       parallelOptions.MaxDegreeOfParallelism = batchSize;

       ServicePointManager.DefaultConnectionLimit = 1000000;

       Parallel.For(0, batchSize, parallelOptions,
           j =>
           {
               Stopwatch sw1 = Stopwatch.StartNew();
               GetDataFromHttpClientAsync<List<AHNData>>(sw1);
           });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                Stopwatch sw1 = Stopwatch.StartNew();
                GetDataFromHttpClientSync<List<AHNData>>(sw1);
            });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                using (WebClient client = new WebClient())
                {
                   Stopwatch sw = Stopwatch.StartNew();
                   byte[] arr = client.DownloadData(_url);
                   sw.Stop();

                   Trace.WriteLine("WebClient Sync " + sw.ElapsedMilliseconds);
                }
           });

           Console.Read();
        }

        public static T GetDataFromWebClient<T>()
        {
            using (var webClient = new WebClient())
            {
                webClient.BaseAddress = _url;
                return JsonConvert.DeserializeObject<T>(
                    webClient.DownloadString(_url));
            }
        }

        public static void GetDataFromHttpClientSync<T>(Stopwatch sw)
        {
            HttpClient httpClient = new HttpClient();
            var response = httpClient.GetAsync(_url).Result;
            var obj = JsonConvert.DeserializeObject<T>(
                response.Content.ReadAsStringAsync().Result);
            sw.Stop();

            Trace.WriteLine("HttpClient Sync " + sw.ElapsedMilliseconds);
        }

        public static void GetDataFromHttpClientAsync<T>(Stopwatch sw)
        {
           HttpClient httpClient = new HttpClient();
           var response = httpClient.GetAsync(_url).ContinueWith(
              (a) => {
                 JsonConvert.DeserializeObject<T>(
                    a.Result.Content.ReadAsStringAsync().Result);
                 sw.Stop();
                 Trace.WriteLine("HttpClient Async " + sw.ElapsedMilliseconds);
              }, TaskContinuationOptions.None);
        }
    }
}

Mis preguntas

  1. Las llamadas REST regresan en 3-4 segundos, lo cual es aceptable. Las llamadas al servicio REST se inician en métodos de controlador que se invocan desde llamadas ajax. Para empezar, las llamadas se ejecutan en un hilo diferente y no bloquea la interfaz de usuario. Entonces, ¿puedo seguir con las llamadas de sincronización?
  2. El código anterior se ejecutó en mi localbox. En la configuración de productos, la búsqueda de DNS y proxy estará involucrada. ¿Hay alguna ventaja de usar HttpClientmás WebClient?
  3. ¿Es HttpClientmejor la concurrencia que WebClient? De los resultados de la prueba, veo que las WebClientllamadas de sincronización funcionan mejor.
  4. ¿ HttpClientSerá una mejor opción de diseño si actualizamos a .Net 4.5? El rendimiento es el factor clave del diseño.
usuario3092913
fuente
55
Su prueba es injusta GetDataFromHttpClientAsyncporque, al ejecutarse primero, las otras invocaciones se benefician de tener datos potencialmente almacenados (ya sea en la máquina local o en cualquier proxy transparente entre usted y el destino) y será más rápido. Además, bajo las condiciones correctas var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result;puede resultar en un punto muerto debido a que agota los hilos del conjunto de hilos. Nunca debe bloquear una actividad que depende del grupo de subprocesos en los subprocesos de ThreadPool, en su awaitlugar debe hacerlo para que devuelva el subproceso al grupo.
Scott Chamberlain
1
HttpClient con Web API Client es fantástico para un cliente JSON / XML REST.
Cory Nelson
@Scott Chamberlain - Gracias por su respuesta. Como todas las llamadas de prueba se ejecutan en paralelo. Por lo tanto, no hay garantía de cuál se ejecutaría primero. Además, si la primera llamada al servicio era de GetDataFromHttpClientAsync, todas las llamadas posteriores de GetDataFromHttpClientAsync deberían haberse beneficiado de la memoria caché y ejecutarse más rápido. No vi eso en el resultado. Rgd espera, todavía estamos usando 4.0. Estoy de acuerdo con usted en que HttpClient de manera sincronizada conduciría a un punto muerto y estoy descartando esa opción fuera de mi consideración de diseño.
user3092913
@CoryNelson ¿Puede explicar por qué HttpClient con Web API Client es fantástico para un cliente JSON / XML REST?
user3092913
2
Aquí hay algunas palabras sobre la diferencia entre HttpClient y WebClient: blogs.msdn.com/b/henrikn/archive/2012/02/11/…
JustAndrei

Respuestas:

243

Vivo en los mundos F # y Web API.

Están sucediendo muchas cosas buenas con la API web, especialmente en forma de controladores de mensajes para seguridad, etc.

Sé que la mía es solo una opinión, pero solo recomendaría usarla HttpClientpara cualquier trabajo futuro . Quizás haya alguna manera de aprovechar algunas de las otras piezas que salen System.Net.Httpsin usar ese ensamblaje directamente, pero no puedo imaginar cómo funcionaría eso en este momento.

Hablando de comparar estos dos

  • HttpClient está más cerca de HTTP que WebClient.
  • HttpClient no estaba destinado a ser un reemplazo completo de Web Client, ya que hay cosas como el progreso del informe, el esquema de URI personalizado y la realización de llamadas FTP que proporciona WebClient, pero HttpClient no lo hace.
+--------------------------------------------+--------------------------------------------+
|               WebClient                    |               HttpClient                   |
+--------------------------------------------+--------------------------------------------+
| Available in older versions of .NET        | .NET 4.5 only.  Created to support the     |
|                                            | growing need of the Web API REST calls     |
+--------------------------------------------+--------------------------------------------+
| WinRT applications cannot use WebClient    | HTTPClient can be used with WinRT          |
+--------------------------------------------+--------------------------------------------+
| Provides progress reporting for downloads  | No progress reporting for downloads        |
+--------------------------------------------+--------------------------------------------+
| Does not reuse resolved DNS,               | Can reuse resolved DNS, cookie             |
| configured cookies                         | configuration and other authentication     |
+--------------------------------------------+--------------------------------------------+
| You need to new up a WebClient to          | Single HttpClient can make concurrent      |
| make concurrent requests.                  | requests                                   |
+--------------------------------------------+--------------------------------------------+
| Thin layer over WebRequest and             | Thin layer of HttpWebRequest and           |
| WebResponse                                | HttpWebResponse                            |
+--------------------------------------------+--------------------------------------------+
| Mocking and testing WebClient is difficult | Mocking and testing HttpClient is easy     |
+--------------------------------------------+--------------------------------------------+
| Supports FTP                               | No support for FTP                         |
+--------------------------------------------+--------------------------------------------+
| Both Synchronous and Asynchronous methods  | All IO bound methods in                    |
| are available for IO bound requests        | HTTPClient are asynchronous                |
+--------------------------------------------+--------------------------------------------+

Si está utilizando .NET 4.5, utilice la bondad asincrónica con HttpClient que Microsoft proporciona a los desarrolladores. HttpClient es muy simétrico para los hermanos del servidor del HTTP, estos son HttpRequest y HttpResponse.

Actualización: 5 razones para usar la nueva API HttpClient:

  • Encabezados fuertemente tipados.
  • Cachés compartidos, cookies y credenciales
  • Acceso a cookies y cookies compartidas.
  • Control sobre el almacenamiento en caché y el caché compartido.
  • Inyecte su módulo de código en la tubería ASP.NET. Código más limpio y modular.

Referencia

C # 5.0 Joseph Albahari

(Channel9 - Video Build 2013)

Cinco grandes razones para usar la nueva API HttpClient para conectarse a servicios web

WebClient vs HttpClient vs HttpWebRequest

Anant Dabhi
fuente
44
Cabe mencionar que HttpClient también está disponible para .NET 4.0 .
Todd Menier
2
Esto no explica por qué WebClient parece ser magnitudes más rápido que HttpClient. También WebClientparece tener métodos asincrónicos ahora.
aplastar
8
@crush es porque el OP está creando una nueva instancia de HttpClient para cada solicitud. En su lugar, debe usar una sola instancia de HttpClient para la vida útil de su aplicación. Ver stackoverflow.com/a/22561368/57369
Gabriel
66
Vale la pena señalar WebClientque no está disponible en .Net Corepero HttpClientestá.
Pranav Singh
3
Desde .Net Core 2.0 WebClient (entre miles de otras API) están de vuelta y disponibles.
CoderBang
56

HttpClient es la más nueva de las API y tiene los beneficios de

  • tiene un buen modelo de programación asíncrona
  • Henrik F Nielson, quien es básicamente uno de los inventores de HTTP, trabajó en él y diseñó la API para que le resulte fácil seguir el estándar HTTP, por ejemplo, generar encabezados que cumplan con los estándares
  • está en .Net Framework 4.5, por lo que tiene cierto nivel de soporte garantizado para el futuro previsible
  • también tiene la versión xcopyable / portable-framework de la biblioteca si desea usarla en otras plataformas: .Net 4.0, Windows Phone, etc.

Si está escribiendo un servicio web que está realizando llamadas REST a otros servicios web, debería utilizar un modelo de programación asíncrono para todas sus llamadas REST, de modo que no se encuentre con el hambre de subprocesos. Probablemente también desee utilizar el compilador C # más nuevo que tiene soporte asíncrono / espera.

Nota: No es más eficiente AFAIK. Probablemente tenga un rendimiento similar si crea una prueba justa.

Tim Lovell-Smith
fuente
Si tuviera una manera de cambiar el proxy, sería una locura
ed22
3

En primer lugar, no soy una autoridad en WebClient vs. HttpClient, específicamente. En segundo lugar, según sus comentarios anteriores, parece sugerir que WebClient es SINCRONIZADO SOLAMENTE mientras que HttpClient es ambos.

Hice una prueba de rendimiento rápida para encontrar cómo funcionan WebClient (llamadas de sincronización), HttpClient (sincronización y sincronización). Y aquí están los resultados.

Veo eso como una gran diferencia cuando se piensa en el futuro, es decir, procesos de larga ejecución, GUI receptiva, etc.

Anthony Horne
fuente
44
WebClientparece tener capacidades asíncronas en las últimas versiones de .NET. Me gustaría saber por qué parece estar superando a HttpClient en una escala tan masiva.
aplastar
1
De acuerdo con stackoverflow.com/a/4988325/1662973 , parece ser lo mismo, aparte del hecho de que uno es una abstracción del otro. Tal vez, depende de cómo se usan / cargan los objetos. El tiempo mínimo admite la afirmación de que el cliente web es, de hecho, una abstracción de HttpClient, por lo que hay una sobrecarga de milisegundos. El marco podría ser "astuto" en cuanto a cómo realmente está agrupando o eliminando al cliente web.
Anthony Horne
2

Tengo un punto de referencia entre HttpClient, WebClient, HttpWebResponse y luego llamo Rest Web Api

y resultado Llamada de referencia web Api Benchmark

--------------------- Etapa 1 ---- 10 Solicitud

{00: 00: 17.2232544} ====> HttpClinet

{00: 00: 04.3108986} ====> Solicitud web

{00: 00: 04.5436889} ====> Cliente web

--------------------- Etapa 1 ---- 10 Solicitud - Tamaño pequeño

{00: 00: 17.2232544} ====> HttpClinet

{00: 00: 04.3108986} ====> Solicitud web

{00: 00: 04.5436889} ====> Cliente web

--------------------- Etapa 3 ---- 10 Solicitud de sincronización - Tamaño pequeño

{00: 00: 15.3047502} ====> HttpClinet

{00: 00: 03.5505249} ====> Solicitud web

{00: 00: 04.0761359} ====> Cliente web

--------------------- Etapa 4 ---- Solicitud de sincronización 100 - Tamaño pequeño

{00: 03: 23.6268086} ====> HttpClinet

{00: 00: 47.1406632} ====> Solicitud web

{00: 01: 01.2319499} ====> Cliente web

--------------------- Etapa 5 ---- 10 Solicitud de sincronización - Tamaño máximo

{00: 00: 58.1804677} ====> HttpClinet

{00: 00: 58.0710444} ====> Solicitud web

{00: 00: 38.4170938} ====> Cliente web

--------------------- Etapa 6 ---- 10 Solicitud de sincronización - Tamaño máximo

{00: 01: 04.9964278} ====> HttpClinet

{00: 00: 59.1429764} ====> Solicitud web

{00: 00: 32.0584836} ====> Cliente web

_____ WebClient es más rápido ()

var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetHttpClient();
            CallPostHttpClient();
        }

        stopWatch.Stop();

        var httpClientValue = stopWatch.Elapsed;

        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetWebRequest();
            CallPostWebRequest();
        }

        stopWatch.Stop();

        var webRequesttValue = stopWatch.Elapsed;


        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {

            CallGetWebClient();
            CallPostWebClient();

        }

        stopWatch.Stop();

        var webClientValue = stopWatch.Elapsed;

// ------------------------- Funciones

private void CallPostHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.PostAsync("PostJson", null);
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private void CallGetHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.GetAsync("getjson");
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private string CallGetWebRequest()
    {
        var request = (HttpWebRequest)WebRequest.Create("https://localhost:44354/api/test/getjson");

        request.Method = "GET";
        request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

        var content = string.Empty;

        using (var response = (HttpWebResponse)request.GetResponse())
        {
            using (var stream = response.GetResponseStream())
            {
                using (var sr = new StreamReader(stream))
                {
                    content = sr.ReadToEnd();
                }
            }
        }

        return content;
    }
    private string CallPostWebRequest()
    {

        var apiUrl = "https://localhost:44354/api/test/PostJson";


        HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(apiUrl));
        httpRequest.ContentType = "application/json";
        httpRequest.Method = "POST";
        httpRequest.ContentLength = 0;

        using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
        {
            using (Stream stream = httpResponse.GetResponseStream())
            {
                var json = new StreamReader(stream).ReadToEnd();
                return json;
            }
        }

        return "";
    }

    private string CallGetWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/getjson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.DownloadString(apiUrl);


        return json;
    }

    private string CallPostWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/PostJson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.UploadString(apiUrl, "");


        return json;
    }
usuario3754071
fuente
1
Ver el comentario de Gabriel arriba. En resumen, HttpClient es mucho más rápido si crea una instancia de HttpClient y la reutiliza.
LT Dan
1

Quizás podrías pensar en el problema de una manera diferente. WebClienty HttpClientson esencialmente implementaciones diferentes de la misma cosa. Lo que recomiendo es implementar el patrón de inyección de dependencia con un contenedor de IoC en toda la aplicación. Debe construir una interfaz de cliente con un mayor nivel de abstracción que la transferencia HTTP de bajo nivel. Puede escribir clases concretas que usen ambos WebClienty HttpClient, y luego usar el contenedor IoC para inyectar la implementación a través de config.

Lo que esto le permitiría hacer sería cambiar HttpClienty cambiar WebClientfácilmente para poder realizar pruebas objetivas en el entorno de producción.

Entonces preguntas como:

¿Será HttpClient una mejor opción de diseño si actualizamos a .Net 4.5?

En realidad, se puede responder objetivamente cambiando entre las dos implementaciones de cliente utilizando el contenedor IoC. Aquí hay una interfaz de ejemplo de la que puede depender que no incluye ningún detalle sobre HttpCliento WebClient.

/// <summary>
/// Dependency Injection abstraction for rest clients. 
/// </summary>
public interface IClient
{
    /// <summary>
    /// Adapter for serialization/deserialization of http body data
    /// </summary>
    ISerializationAdapter SerializationAdapter { get; }

    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <typeparam name="TRequestBody">The type of the request body if specified</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns></returns>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(Request<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Default timeout for http requests
    /// </summary>
    TimeSpan Timeout { get; set; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    Uri BaseUri { get; set; }

    /// <summary>
    /// Name of the client
    /// </summary>
    string Name { get; }
}

public class Request<TRequestBody>
{
    #region Public Properties
    public IHeadersCollection Headers { get; }
    public Uri Resource { get; set; }
    public HttpRequestMethod HttpRequestMethod { get; set; }
    public TRequestBody Body { get; set; }
    public CancellationToken CancellationToken { get; set; }
    public string CustomHttpRequestMethod { get; set; }
    #endregion

    public Request(Uri resource,
        TRequestBody body,
        IHeadersCollection headers,
        HttpRequestMethod httpRequestMethod,
        IClient client,
        CancellationToken cancellationToken)
    {
        Body = body;
        Headers = headers;
        Resource = resource;
        HttpRequestMethod = httpRequestMethod;
        CancellationToken = cancellationToken;

        if (Headers == null) Headers = new RequestHeadersCollection();

        var defaultRequestHeaders = client?.DefaultRequestHeaders;
        if (defaultRequestHeaders == null) return;

        foreach (var kvp in defaultRequestHeaders)
        {
            Headers.Add(kvp);
        }
    }
}

public abstract class Response<TResponseBody> : Response
{
    #region Public Properties
    public virtual TResponseBody Body { get; }

    #endregion

    #region Constructors
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response() : base()
    {
    }

    protected Response(
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    TResponseBody body,
    Uri requestUri
    ) : base(
        headersCollection,
        statusCode,
        httpRequestMethod,
        responseData,
        requestUri)
    {
        Body = body;
    }

    public static implicit operator TResponseBody(Response<TResponseBody> readResult)
    {
        return readResult.Body;
    }
    #endregion
}

public abstract class Response
{
    #region Fields
    private readonly byte[] _responseData;
    #endregion

    #region Public Properties
    public virtual int StatusCode { get; }
    public virtual IHeadersCollection Headers { get; }
    public virtual HttpRequestMethod HttpRequestMethod { get; }
    public abstract bool IsSuccess { get; }
    public virtual Uri RequestUri { get; }
    #endregion

    #region Constructor
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response()
    {
    }

    protected Response
    (
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    Uri requestUri
    )
    {
        StatusCode = statusCode;
        Headers = headersCollection;
        HttpRequestMethod = httpRequestMethod;
        RequestUri = requestUri;
        _responseData = responseData;
    }
    #endregion

    #region Public Methods
    public virtual byte[] GetResponseData()
    {
        return _responseData;
    }
    #endregion
}

Código completo

Implementación de HttpClient

Puede utilizar Task.Runpara WebClientejecutar de forma asincrónica en su implementación.

La inyección de dependencia, cuando se hace bien, ayuda a aliviar el problema de tener que tomar decisiones de bajo nivel por adelantado. En última instancia, la única forma de saber la verdadera respuesta es intentarlo en un entorno en vivo y ver cuál funciona mejor. Es muy posible que WebClientfuncione mejor para algunos clientes y HttpClientque funcione mejor para otros. Por eso es importante la abstracción. Significa que el código puede intercambiarse rápidamente o cambiarse con la configuración sin cambiar el diseño fundamental de la aplicación.

Melbourne Developer
fuente
0

Opinión impopular de 2020:

Cuando se trata de aplicaciones ASP.NET , todavía prefiero WebClienta las siguientes HttpClientporque:

  1. La implementación moderna viene con métodos asíncronos / esperables basados ​​en tareas
  2. Tiene una huella de memoria más pequeña y 2x-5x más rápido (otras respuestas ya mencionan eso) especialmente en escenarios en los que simplemente no puede " reutilizar una sola instancia de HttpClient para la vida útil de su aplicación " como recomiendan otros comentaristas. Y ASP.NET es uno de esos escenarios: no hay una "duración de la aplicación", solo la duración de una solicitud.
Alex
fuente