在HttpClient和WebClient之间进行选择

我们的web应用程序运行在。net Framework 4.0中。UI通过Ajax调用调用控制器方法。

我们需要使用来自供应商的REST服务。我正在评估在。net 4.0中调用REST服务的最佳方式。REST服务需要一个基本的身份验证方案,它可以返回XML和JSON两种格式的数据。

对上传/下载大数据没有任何要求,我认为未来也不会有任何要求。我查看了一些用于REST消费的开源代码项目,并没有发现它们有任何价值来证明项目中的额外依赖。我开始计算WebClientHttpClient。我从NuGet下载了。net 4.0的HttpClient。

我搜索了WebClientHttpClient这个网站之间的差异,其中提到单个HttpClient可以处理并发调用,并且可以重用已解析的DNS、cookie配置和身份验证。我还没有看到我们可能从这些差异中获得的实际价值。

我做了一个快速的性能测试,以了解WebClient(同步调用),HttpClient(同步和异步)的执行情况。结果如下:

我对所有请求使用相同的HttpClient实例(最小值-最大值)。

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

为每个请求(最小值-最大值)使用一个新的HttpClient:

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

代码

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

我的问题

  1. REST调用在3-4秒内返回,这是可以接受的。对REST的调用 服务在被调用的控制器方法中初始化 Ajax调用。首先,调用在不同的线程中运行,不会阻塞UI。那么,我能坚持使用同步调用吗?李< / > 以上代码在我的localbox中运行。在生产设置中,DNS和代理 会涉及到查找。使用HttpClientWebClient有优势吗?李< / >
  2. HttpClient并发性比WebClient更好吗?从测试结果中,我看到WebClient同步调用性能更好。
  3. 如果我们升级到。net 4.5, HttpClient会是更好的设计选择吗?性能是关键的设计因素。
216500 次浏览

首先,我并不是WebClient vs. HttpClient的权威。其次,从你上面的评论来看,似乎表明WebClient是同步的只有,而HttpClient两者都是。

我做了一个快速的性能测试,以了解WebClient(同步调用),HttpClient(同步和异步)的执行情况。这是结果。

在考虑未来时,我认为这是一个巨大的差异,即长时间运行的进程,响应式GUI等(加上你建议的。net框架4.5的好处-根据我的实际经验,它在IIS上要快得多)。

HttpClient是较新的api,它的优点是

  • 拥有良好的异步编程模型
  • Henrik F Nielson是HTTP的发明者之一,他设计的API让你很容易遵循HTTP标准,例如生成符合标准的头文件
  • 是在.NET框架4.5中,所以它在可预见的未来有一定程度的支持
  • 也有选择复制文件able/portable-framework版本的库,如果你想在其他平台上使用它- . net 4.0, Windows Phone等。

如果您正在编写一个web服务,该服务对其他web服务进行REST调用,那么您应该对所有的REST调用使用异步编程模型,这样您就不会遇到线程饥饿。你可能还想使用最新的c#编译器,它有异步/等待支持。

注意:AFAIK,它并不是性能更好。如果你创建一个公平的测试,它可能会有类似的表现。

我已经在HttpClient、WebClient和HttpWebResponse之间进行了基准测试,然后调用REST Web API。

结果是:

调用REST Web API基准

---------------------Stage 1  ---- 10 Request


{00:00:17.2232544} ====>HttpClinet
{00:00:04.3108986} ====>WebRequest
{00:00:04.5436889} ====>WebClient


---------------------Stage 1  ---- 10 Request--Small Size
{00:00:17.2232544}====>HttpClinet
{00:00:04.3108986}====>WebRequest
{00:00:04.5436889}====>WebClient


---------------------Stage 3  ---- 10 sync Request--Small Size
{00:00:15.3047502}====>HttpClinet
{00:00:03.5505249}====>WebRequest
{00:00:04.0761359}====>WebClient


---------------------Stage 4  ---- 100 sync Request--Small Size
{00:03:23.6268086}====>HttpClinet
{00:00:47.1406632}====>WebRequest
{00:01:01.2319499}====>WebClient


---------------------Stage 5  ---- 10 sync Request--Max Size


{00:00:58.1804677}====>HttpClinet
{00:00:58.0710444}====>WebRequest
{00:00:38.4170938}====>WebClient


---------------------Stage 6  ---- 10 sync Request--Max Size


{00:01:04.9964278}====>HttpClinet
{00:00:59.1429764}====>WebRequest
{00:00:32.0584836}====>WebClient

WebClient更快

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;


//-------------------------Functions


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

也许你可以用另一种方式来思考这个问题。WebClientHttpClient本质上是同一事物的不同实现。我建议在整个应用程序中使用IoC容器实现依赖注入模式。您应该构造一个抽象级别高于低级HTTP传输的客户机接口。你可以编写既使用WebClient又使用HttpClient的具体类,然后使用IoC容器通过配置注入实现。

这将允许您在HttpClientWebClient之间轻松切换,以便您能够在生产环境中客观地进行测试。

像这样的问题:

如果我们升级到。net 4.5, HttpClient会是更好的设计选择吗?

实际上可以通过使用IoC容器在两个客户机实现之间切换来客观地回答。这里是一个你可能依赖的示例接口,它不包括任何关于HttpClientWebClient的详细信息。

/// <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
}

完整代码

HttpClient Implementation

可以使用Task.Run使WebClient在实现中异步运行。

依赖注入,如果做得好,有助于减轻不得不预先做出低级决策的问题。最终,知道真正答案的唯一方法是在实际环境中尝试这两种方法,看看哪一种效果最好。WebClient可能更适合某些客户,而HttpClient可能更适合其他客户。这就是为什么抽象很重要。这意味着代码可以快速交换,或者在不改变应用程序基本设计的情况下根据配置进行更改。

顺便说一句:你应该使用抽象而不是直接调用这些低级api的原因还有很多。一个巨大的问题是单元可测试性。

HttpClientFactory

评估创建HttpClient的不同方法是很重要的,其中一部分就是理解HttpClientFactory。

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

我知道这不是一个直接的答案——但你最好从这里开始,而不是到处都是new HttpClient(...)

当涉及到ASP。网络应用程序时,我仍然更喜欢WebClient而不是HttpClient,因为:

  1. 现代实现带有异步/可等待的基于任务的方法
  2. 内存占用更小,速度快2-5倍(其他答案已经提到了这一点)
  3. 建议重复使用单个 "但 ASP。NET没有“应用程序的生命周期”,只有请求的生命周期。目前ASP。NET 5将使用HttpClientFactory,但它只能通过依赖注入使用。有些人想要一个更简单的解决方案
  4. 最重要的是,如果你像MS建议的那样在应用的生命周期中使用一个HttpClient的单例实例——它有已知的问题。例如DNS缓存问题——HttpClient简单地忽略TTL并“永远”缓存DNS。不过,也有变通办法。如果你想了解更多关于HttpClient的问题和困惑,请在Microsoft GitHub上阅读这样的评论