روش استفادهی صحیح از HttpClient در برنامههای دات نت
نویسنده: وحید نصیری
تاریخ: ۱۳۹۶/۱۰/۲۱ ۱۲:۲۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
using(var client = new HttpClient())
{
// do something with http client
} Unable to connect to the remote server System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://example.com/");
} for (int i = 0; i < 10; i++)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://example.com/");
Console.WriteLine(result.StatusCode);
}
} TCP 192.168.1.6:13996 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13997 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13998 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13999 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14000 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14001 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14002 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14003 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14004 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14005 93.184.216.34:http TIME_WAIT
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]
CancelPendingRequests DeleteAsync GetAsync GetByteArrayAsync GetStreamAsync GetStringAsync PostAsync PutAsync SendAsync
BaseAddress DefaultRequestHeaders MaxResponseContentBufferSize Timeout
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com"));
sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds using System;
using System.Collections.Generic;
using System.Net.Http;
namespace HttpClientTips
{
public interface IHttpClientFactory : IDisposable
{
HttpClient GetOrCreate(
Uri baseAddress,
IDictionary<string, string> defaultRequestHeaders = null,
TimeSpan? timeout = null,
long? maxResponseContentBufferSize = null,
HttpMessageHandler handler = null);
}
} using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
namespace HttpClientTips
{
/// <summary>
/// Lifetime of this class should be set to `Singleton`.
/// </summary>
public class HttpClientFactory : IHttpClientFactory
{
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
// threads but only one of the objects succeeds in creating the HttpClient.
private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients =
new ConcurrentDictionary<Uri, Lazy<HttpClient>>();
private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute
public HttpClientFactory()
{
// Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx
ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
// Increases the concurrent outbound connections
ServicePointManager.DefaultConnectionLimit = 1024;
}
public HttpClient GetOrCreate(
Uri baseAddress,
IDictionary<string, string> defaultRequestHeaders = null,
TimeSpan? timeout = null,
long? maxResponseContentBufferSize = null,
HttpMessageHandler handler = null)
{
return _httpClients.GetOrAdd(baseAddress,
uri => new Lazy<HttpClient>(() =>
{
// Reusing a single HttpClient instance across a multi-threaded application means
// you can't change the values of the stateful properties (which are not thread safe),
// like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout.
// So you can only use them if they are constant across your application and need their own instance if being varied.
var client = handler == null ? new HttpClient { BaseAddress = baseAddress } :
new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress };
setRequestTimeout(timeout, client);
setMaxResponseBufferSize(maxResponseContentBufferSize, client);
setDefaultHeaders(defaultRequestHeaders, client);
setConnectionLeaseTimeout(baseAddress, client);
return client;
},
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}
public void Dispose()
{
foreach (var httpClient in _httpClients.Values)
{
httpClient.Value.Dispose();
}
}
private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client)
{
// This ensures connections are used efficiently but not indefinitely.
client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client
ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely.
}
private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client)
{
if (defaultRequestHeaders == null)
{
return;
}
foreach (var item in defaultRequestHeaders)
{
client.DefaultRequestHeaders.Add(item.Key, item.Value);
}
}
private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client)
{
if (maxResponseContentBufferSize.HasValue)
{
client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value;
}
}
private static void setRequestTimeout(TimeSpan? timeout, HttpClient client)
{
if (timeout.HasValue)
{
client.Timeout = timeout.Value;
}
}
}
} namespace HttpClientTips.Web
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
services.AddMvc();
} namespace HttpClientTips.Web.Controllers
{
public class HomeController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public HomeController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<IActionResult> Index()
{
var host = new Uri("http://localhost:5000");
var httpClient = _httpClientFactory.GetOrCreate(host);
var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false);
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
return Content(responseContent);
} If multiple operations complete at once for the same application,
AspNetSynchronizationContext will ensure that they execute one at a
time. They may execute on any thread, but that thread will have the
identity and culture of the original page
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
});
services.AddHttpClient();
} IHttpClientFactory _httpClientFactory;
public MyController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public IActionResult Index()
{
//This client doesn’t have any special configuration applied
var defaultClient = _httpClientFactory.CreateClient();
//This client has the header and base address configured for the “github” client above.
var gitHubClient = _httpClientFactory.CreateClient("github");
return View();
} PM> Install-Package DNTCommon.Web.Core
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); httpRequestMessage.Headers.Authorization = ... httpClient.SendAsync(httpRequestMessage);
private async Task<(Token Token, Dictionary<string, string> AppCookies)> LoginAsync(string requestUri, string username, string password)
{
var viewmodel = new LoginViewModel { Username = username, Password = password };
var host = new Uri("http://localhost:5000");
var httpClient = _httpClientFactoryService.GetOrCreate(host);
var responseMessage = await httpClient.PostAsJsonAsync("api/account/login", viewmodel).ConfigureAwait(false);
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
// return Content(responseContent);
...
} //Note: First you should run the `ASPNETCore2JwtAuthentication.WebApp` project and then run the `ConsoleClient` project.
یک نکتهی تکمیلی: امکان کار همزمان هم با HttpClient وجود دارد!
تا پیش از ارائهی NET Core.، روش متداول دریافت فایلها، عموما به صورت زیر و همزمان/synchronous بود:
var client = new WebClient(); client.DownloadFile(downloadUrl, filePath);
هرچند ... WebClient امکان دریافت فایلها را به صورت غیرهمزمان هم دارد، اما API آن با async/await هماهنگ نیست و طراحی آن قدیمی است.
پس از آن، HttpClient ارائه شد که از روز اول، async بود و کاملا هماهنگ با async/await و روش کدنویسی جدید آن. اما ... شاید در قسمتهایی نیاز باشد تا بتوان کدهای قدیمی را بدون تبدیل کردن آنها به نمونههای async، به همان شکل همزمان، بازنویسی کنیم. برای رفع این مشکل، از زمان داتنت 5، متد Send همزمان هم به API آن اضافه شدهاست:
var response = httpClient.Send(new HttpRequestMessage(HttpMethod.Post, "http://site.com")); using var reader = new StreamReader(response.Content.ReadAsStream()); var content = reader.ReadToEnd();