Decidindo entre HttpClient e WebClient

218

Nosso aplicativo da web está sendo executado no .Net Framework 4.0. A interface do usuário chama métodos de controlador por meio de chamadas ajax.

Precisamos consumir o serviço REST do nosso fornecedor. Estou avaliando a melhor maneira de chamar o serviço REST no .Net 4.0. O serviço REST requer Esquema de autenticação básica e pode retornar dados em XML e JSON. Não há necessidade de fazer upload / download de grandes dados e não vejo nada no futuro. Analisei alguns projetos de código-fonte aberto para consumo REST e não encontrei nenhum valor para justificar dependência adicional no projeto. Começou a avaliar WebCliente HttpClient. Eu baixei o HttpClient for .Net 4.0 do NuGet.

Eu procurei por diferenças entre WebCliente HttpCliente neste site mencionado esse único HttpClient pode manipular chamadas simultâneas e pode reutilizar resolvido DNS, configuração de cookies e autenticação. Ainda estou para ver valores práticos que podemos obter devido às diferenças.

Fiz um teste rápido de desempenho para descobrir como WebClient(chamadas de sincronização), HttpClient(sincronização e assíncrona) se comportam. e aqui estão os resultados:

Usando a mesma HttpClientinstância para todas as solicitações (mín. - máx.)

Sincronização do WebClient: 8 ms - 167 ms
Sincronização do HttpClient: 3 ms - 7228 ms
Assinatura do HttpClient: 985 - 10405 ms

Usando um novo HttpClientpara cada solicitação (min - max)

Sincronização de cliente da Web: 4 ms - 297 ms
Sincronização de HttpClient: 3 ms - 7953 ms
Assinatura de HttpClient: 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);
        }
    }
}

Minhas perguntas

  1. As chamadas REST retornam em 3-4s, o que é aceitável. As chamadas para o serviço REST são iniciadas nos métodos do controlador que são chamados a partir de chamadas ajax. Para começar, as chamadas são executadas em um thread diferente e não bloqueiam a interface do usuário. Então, posso ficar com as chamadas de sincronização?
  2. O código acima foi executado no meu localbox. Na configuração do produto, a pesquisa de DNS e proxy estará envolvida. Existe alguma vantagem em usar HttpClientmais WebClient?
  3. A HttpClientconcorrência é melhor que WebClient? A partir dos resultados do teste, vejo que as WebClientchamadas de sincronização têm melhor desempenho.
  4. Será HttpClientuma escolha de design melhor se atualizarmos para o .Net 4.5? O desempenho é o principal fator de design.
user3092913
fonte
5
Seu teste é injusto, GetDataFromHttpClientAsyncporque é executado primeiro, as outras invocações se beneficiam de ter dados coletados em potencial (seja na máquina local ou em qualquer proxy transparente entre você e o destino) e serão mais rápidos. Além disso, nas condições corretas, var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result;pode resultar em um impasse devido ao esgotamento dos encadeamentos do conjunto de encadeamentos. Você nunca deve bloquear uma atividade que dependa do conjunto de encadeamentos nos encadeamentos ThreadPool; em awaitvez disso, retorne o encadeamento novamente para o conjunto.
Scott Chamberlain
1
O HttpClient com o Web API Client é fantástico para um cliente REST JSON / XML.
Cory Nelson
@ Scott Chamberlain - Obrigado pela sua resposta. Como todas as chamadas de teste são executadas no Parallel.Foreach, não há garantia de qual delas seria executada primeiro. Além disso, se a primeira chamada para o serviço fosse de GetDataFromHttpClientAsync, todas as chamadas subsequentes de GetDataFromHttpClientAsync deveriam se beneficiar do cache e executar mais rapidamente. Não vi isso no resultado. Aguarde, ainda estamos usando o 4.0. Concordo com você que o HttpClient de maneira sincronizada levaria a um impasse e estou descartando essa opção da minha consideração de design.
user3092913
@CoryNelson Você pode explicar por que o HttpClient com Web API Client é fantástico para um cliente REST JSON / XML?
user3092913
2
Aqui estão algumas palavras sobre a diferença entre HttpClient e WebClient: blogs.msdn.com/b/henrikn/archive/2012/02/11/...
JustAndrei

Respostas:

243

Eu moro no mundo do F # e da API da Web.

Há muitas coisas boas acontecendo com a API da Web, especialmente na forma de manipuladores de mensagens para segurança, etc.

Sei que a minha é apenas uma opinião, mas eu recomendaria apenas o uso HttpClientpara trabalhos futuros . Talvez haja alguma maneira de aproveitar algumas das outras peças que saem System.Net.Httpsem usar diretamente essa montagem, mas não consigo imaginar como isso funcionaria no momento.

Falando em comparar esses dois

  • O HttpClient está mais próximo do HTTP do que o WebClient.
  • O HttpClient não era para ser uma substituição completa do Web Client, pois existem coisas como progresso do relatório, esquema de URI personalizado e chamadas de FTP que o WebClient fornece - mas o HttpClient não.
+--------------------------------------------+--------------------------------------------+
|               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                |
+--------------------------------------------+--------------------------------------------+

Se você estiver usando o .NET 4.5, use a bondade assíncrona com o HttpClient que a Microsoft fornece aos desenvolvedores. HttpClient é muito simétrico aos irmãos do HTTP do lado do servidor, que são HttpRequest e HttpResponse.

Atualização: 5 razões para usar a nova API HttpClient:

  • Cabeçalhos fortemente tipados.
  • Caches, cookies e credenciais compartilhados
  • Acesso a cookies e cookies compartilhados
  • Controle sobre cache e cache compartilhado.
  • Injete seu módulo de código no pipeline do ASP.NET. Código mais limpo e modular.

Referência

C # 5.0 Joseph Albahari

(Channel9 - Compilação de vídeo 2013)

Cinco grandes motivos para usar a nova API HttpClient para conectar-se a serviços da Web

HttpClient vs WebClient vs HttpWebRequest

Anant Dabhi
fonte
4
Deve-se mencionar que o HttpClient também está disponível para o .NET 4.0 .
Todd Menier
2
Isso não explica por que o WebClient parece ser mais rápido que o HttpClient. Também WebClientparece ter métodos assíncronos agora.
esmagar
8
@crush é porque o OP está criando uma nova instância do HttpClient para cada solicitação. Em vez disso, você deve usar uma única instância do HttpClient durante toda a vida útil do seu aplicativo. Veja stackoverflow.com/a/22561368/57369
Gabriel
6
Vale a pena notar que WebClientnão está disponível, .Net Coremas HttpClientestá.
Pranav Singh
3
Como o .Net Core 2.0 WebClient (entre milhares de outras APIs) está de volta e disponível.
precisa saber é o seguinte
56

HttpClient é a mais nova das APIs e possui os benefícios de

  • tem um bom modelo de programação assíncrona
  • sendo trabalhado por Henrik F Nielson, que é basicamente um dos inventores do HTTP, e ele projetou a API, para que seja fácil seguir o padrão HTTP, por exemplo, gerar cabeçalhos em conformidade com os padrões
  • está no .net framework 4.5, por isso tem algum nível garantido de suporte no futuro próximo
  • também possui a versão xcopyable / portable-framework da biblioteca, se você quiser usá-la em outras plataformas - .Net 4.0, Windows Phone etc.

Se você estiver gravando um serviço da Web que está fazendo chamadas REST para outros serviços da Web, convém usar um modelo de programação assíncrona para todas as suas chamadas REST, para não atingir a falta de thread. Você provavelmente também deseja usar o compilador C # mais recente, que possui suporte a assíncrono / espera.

Nota: Não é um AFAIK com melhor desempenho. Provavelmente é um desempenho semelhante se você criar um teste justo.

Tim Lovell-Smith
fonte
Se tivesse uma forma de procuração interruptor seria insano
ed22
3

Em primeiro lugar, eu não sou uma autoridade no WebClient vs. HttpClient, especificamente. Em segundo lugar, pelos seus comentários acima, parece sugerir que o WebClient é SOMENTE Sincronizado, enquanto HttpClient é ambos.

Fiz um teste rápido de desempenho para descobrir o desempenho do WebClient (chamadas de sincronização), HttpClient (Sync e Async). e aqui estão os resultados.

Eu vejo isso como uma enorme diferença ao pensar no futuro, ou seja, processos de longa execução, GUI responsiva etc. (adicione o benefício que você sugere na estrutura 4.5 - que na minha experiência real é muito mais rápida no IIS)

Anthony Horne
fonte
4
WebClientparece ter recursos assíncronos nas últimas versões do .NET. Eu gostaria de saber por que parece estar superando o HttpClient em uma escala tão grande.
esmagar
1
De acordo com stackoverflow.com/a/4988325/1662973 , parece ser o mesmo, exceto pelo fato de um ser uma abstração do outro. Talvez isso dependa de como os objetos são usados ​​/ carregados. O tempo mínimo suporta a afirmação de que o webclient é de fato uma abstração do HttpClient; portanto, há um milissegundo de sobrecarga. A estrutura pode estar sendo "sorrateira" em como realmente agrupa ou descarta o cliente da web.
Anthony Horne
2

Eu tenho benchmark entre HttpClient, WebClient, HttpWebResponse e chamo Rest Web Api

e resultado Chamar referência de API da Web Rest Rest

--------------------- Etapa 1 ---- 10 Solicitação

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

{00: 00: 04.3108986} ====> WebRequest

{00: 00: 04.5436889} ====> WebClient

--------------------- Etapa 1 ---- 10 Pedido - Tamanho pequeno

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

{00: 00: 04.3108986} ====> WebRequest

{00: 00: 04.5436889} ====> WebClient

--------------------- Etapa 3 ---- 10 Solicitação de sincronização - Tamanho pequeno

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

{00: 00: 03.5505249} ====> WebRequest

{00: 00: 04.0761359} ====> WebClient

--------------------- Etapa 4 ---- Pedido de sincronização 100 - Tamanho pequeno

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

{00: 00: 47.1406632} ====> WebRequest

{00: 01: 01.2319499} ====> WebClient

--------------------- Etapa 5 ---- 10 Solicitação de sincronização - Tamanho máximo

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

{00: 00: 58.0710444} ====> WebRequest

{00: 00: 38.4170938} ====> WebClient

--------------------- Etapa 6 ---- 10 Solicitação de sincronização - Tamanho máximo

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

{00: 00: 59.1429764} ====> WebRequest

{00: 00: 32.0584836} ====> WebClient

_____ WebClient É mais 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;

//-------------------------Funções

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;
    }
user3754071
fonte
1
Veja o comentário de Gabriel acima. Em resumo, o HttpClient é muito mais rápido se você criar uma instância do HttpClient e reutilizá-la.
LT Dan
1

Talvez você possa pensar sobre o problema de uma maneira diferente. WebCliente HttpClientsão implementações essencialmente diferentes da mesma coisa. O que eu recomendo é implementar o padrão de injeção de dependência com um contêiner de IoC em todo o aplicativo. Você deve construir uma interface do cliente com um nível de abstração mais alto que a transferência HTTP de baixo nível. Você pode escrever classes concretas que usam ambos WebCliente HttpClient, e depois usar o contêiner IoC para injetar a implementação via config.

O que isso permitiria é alternar entre HttpCliente WebClientfacilmente, para que você possa testar objetivamente no ambiente de produção.

Então, perguntas como:

O HttpClient será uma opção de design melhor se atualizarmos para o .Net 4.5?

Na verdade, pode ser respondido objetivamente alternando entre as duas implementações do cliente usando o contêiner IoC. Aqui está um exemplo de interface da qual você pode depender, que não inclui nenhum detalhe sobre HttpClientou 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

Implementação HttpClient

Você pode usar Task.Runpara WebClientexecutar de forma assíncrona em sua implementação.

A injeção de dependência, quando bem feita, ajuda a aliviar o problema de ter que tomar decisões de baixo nível antecipadamente. Por fim, a única maneira de saber a resposta verdadeira é tentar em um ambiente ao vivo e ver qual funciona melhor. É bem possível que WebClientfuncione melhor para alguns clientes e HttpClientfuncione melhor para outros. É por isso que a abstração é importante. Isso significa que o código pode ser rapidamente trocado ou alterado com a configuração sem alterar o design fundamental do aplicativo.

Melbourne Developer
fonte
0

Opinião impopular de 2020:

Quando se trata de ASP.NET aplicativos eu ainda prefiro WebClientmais HttpClientporque:

  1. A implementação moderna vem com métodos baseados em tarefas assíncronas / aguardáveis
  2. Possui área de memória menor e 2x-5x mais rápido (outras respostas já mencionam isso), especialmente em cenários em que você simplesmente não pode " reutilizar uma única instância do HttpClient durante a vida útil do seu aplicativo ", como recomendam outros comentaristas. E o ASP.NET é um desses cenários - não existe "vida útil do aplicativo", apenas vida útil de uma solicitação.
Alex
fonte