[C#] HttpClient와 HttpClientHandler를 요청 사이에 배치해야합니까?

System.Net.Http.HttpClientSystem.Net.Http.HttpClientHandler.NET Framework 4.5의 는 IDisposable ( System.Net.Http.HttpMessageInvoker 를 통해 )을 구현 합니다.

그만큼 using문 문서는 말합니다 :

일반적으로 IDisposable 객체를 사용할 때는 using 문에서 선언하고 인스턴스화해야합니다.

이 답변 은 다음 패턴을 사용합니다.

var baseAddress = new Uri("http://example.com");
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("foo", "bar"),
        new KeyValuePair<string, string>("baz", "bazinga"),
    });
    cookieContainer.Add(baseAddress, new Cookie("CookieName", "cookie_value"));
    var result = client.PostAsync("/test", content).Result;
    result.EnsureSuccessStatusCode();
}

그러나 Microsoft에서 가장 눈에 띄는 예제는 Dispose()명시 적 또는 암시 적으로 호출되지 않습니다 . 예를 들어 :

에서 발표 의 의견을이야, 누군가가 마이크로 소프트 직원에게 물었다 :

샘플을 확인한 후 HttpClient 인스턴스에서 처리 작업을 수행하지 않은 것으로 나타났습니다. 내 응용 프로그램에서 문을 사용하여 HttpClient의 모든 인스턴스를 사용했으며 HttpClient가 IDisposable 인터페이스를 구현하기 때문에 올바른 방법이라고 생각했습니다. 내가 올바른 길을 가고 있습니까?

그의 대답은 다음과 같습니다.

.Net 4에서 실제로 혼합되지 않으므로 “using”및 비동기에주의해야하지만 일반적으로 옳습니다. .Net 4.5에서는 “using”문에서 “await”를 사용할 수 있습니다.

Btw, 당신은 여러 번 같은 HttpClient를 재사용 할 수 있습니다. 일반적으로 당신은 그것들을 항상 생성 / 폐기하지 않을 것입니다.

두 번째 단락은 HttpClient 인스턴스를 몇 번 사용할 수 있는지에 대해 걱정하지 않고 더 이상 필요하지 않은 후에 폐기해야하는 경우에 관한 질문입니다.

(업데이트 : 사실 @DPeden이 제공 한 두 번째 단락이 답의 열쇠입니다.)

그래서 내 질문은 :

  1. 현재 구현 (.NET Framework 4.5)에서 HttpClient 및 HttpClientHandler 인스턴스에서 Dispose ()를 호출해야합니까? 설명 : “필수”란 리소스 누수 또는 데이터 손상 위험과 같이 폐기하지 않은 부정적인 결과가있는 경우를 의미합니다.

  2. 필요하지 않은 경우 IDisposable을 구현하기 때문에 어쨌든 “좋은 습관”이 될 것입니까?

  3. 필요하거나 권장되는 경우 위에서 언급 한이 코드 가 안전하게 구현됩니다 (.NET Framework 4.5의 경우)?

  4. 이러한 클래스가 Dispose ()를 호출 할 필요가없는 경우 왜 IDisposable로 구현 되었습니까?

  5. 필요한 경우 또는 권장되는 경우 Microsoft 예제가 오도되거나 안전하지 않습니까?



답변

일반적인 합의는 HttpClient를 폐기 할 필요가 없다는 것입니다.

그것이 작동하는 방식에 밀접하게 관여하는 많은 사람들이 이것을 언급했습니다.

참조 대럴 밀러의 블로그 게시물 과 관련 SO 게시물 : HttpClient를이 메모리 누수의 결과를 크롤링 참조.

또한 ASP.NET 을 사용 하여 Evolvable Web APIs 디자인 의 HttpClient 장 을 읽고 여기에서 인용되는 “라이프 사이클”섹션에 대한 내용을 읽어 볼 것을 강력히 제안합니다 .

HttpClient는 IDisposable 인터페이스를 간접적으로 구현하지만 HttpClient의 표준 사용법은 모든 요청 후에이를 폐기하지 않습니다. HttpClient 객체는 애플리케이션이 HTTP 요청을해야하는 한 지속되도록 설계되었습니다. 여러 요청에 객체가 있으면 DefaultRequestHeaders를 설정할 수 있으며 HttpWebRequest에 필요한 모든 요청에서 CredentialCache 및 CookieContainer와 같은 항목을 다시 지정하지 않아도됩니다.

또는 DotPeek을 열 수도 있습니다.


답변

현재 답변은 약간 혼란스럽고 오해의 소지가 있으며 중요한 DNS 영향이 누락되었습니다. 나는 상황이 분명하게 어디에 있는지 요약하려고 노력할 것입니다.

  1. 일반적으로 말하면 대부분의 IDisposable객체 , 특히 명명 된 / 공유 OS 리소스소유 한 객체 를 처리 할 때는 이상적으로 처리해야 합니다 . Darrel MillerHttpClient 로서 지적했듯이 취소 토큰을 할당하고 요청 / 응답 본문이 관리되지 않는 스트림 일 수 .
  2. 그러나 HttpClient모범 사례에 따르면 인스턴스를 하나 만들고 최대한 많이 재사용해야합니다 ( 다중 스레드 시나리오에서 스레드 안전 멤버 사용 ). 따라서 대부분의 시나리오에서는 항상 필요할 것이므로 단순히 폐기하지 않습니다 .
  3. 동일한 HttpClient를 “영원히”재사용 할 때의 문제점 은 기본 HTTP 연결이 DNS 변경에 관계없이 원래 DNS 확인 IP에 대해 열려있을 수 있다는 것 입니다. 이는 블루 / 그린 배포 및 DNS 기반 장애 조치와 같은 시나리오에서 문제가 될 수 있습니다 . 이 문제를 처리하기위한 다양한 접근 방법이 Connection:close있습니다. DNS 변경이 발생한 후 서버가 헤더를 전송하는 가장 안정적인 방법 입니다. 또 다른 가능성은 HttpClient클라이언트 측에서 주기적으로 또는 DNS 변경에 대해 학습하는 메커니즘을 통해 클라이언트 를 재활용하는 것입니다. 자세한 내용은 https://github.com/dotnet/corefx/issues/11224 를 참조하십시오 (링크 된 블로그 게시물에서 제안 된 코드를 맹목적으로 사용하기 전에주의 깊게 읽으십시오).

답변

필자는 이해하기 위해 Dispose()나중에 필요한 리소스를 잠글 때만 호출 이 필요합니다 (특정 연결처럼). 더 이상 필요하지 않더라도 더 이상 사용하지 않는 리소스는 항상 사용하지 않는 것이 좋습니다. 일반적으로 사용하지 않는 리소스를 사용하지 않아야하기 때문입니다.

Microsoft 예제가 반드시 정확하지는 않습니다. 응용 프로그램이 종료되면 사용 된 모든 리소스가 해제됩니다. 그리고이 예제의 경우, HttpClient사용이 완료된 직후에 발생합니다 . 마찬가지로 명시 적으로 전화하는 Dispose()것은 다소 불필요한 것입니다.

그러나 일반적으로 클래스가 구현할 때는 완전히 준비되고 가능한 한 빨리 인스턴스를 IDisposable이해해야합니다 Dispose(). HttpClient리소스 나 연결이 유지 / 개방되는지 여부에 대해 명시 적으로 문서화되어 있지 않은 경우에 특히 그렇습니다 . 연결이 다시 재사용되는 경우 [곧]Dipose() 것입니다.이 경우 “완전히 준비되지 않았습니다”.

참조 :
IDisposable.Dispose 메서드Dispose 호출시기


답변

Dispose ()는 아래 코드를 호출하여 HttpClient 인스턴스에서 연 연결을 닫습니다. 이 코드는 dotPeek으로 디 컴파일하여 작성되었습니다.

HttpClientHandler.cs-폐기

ServicePointManager.CloseConnectionGroups(this.connectionGroupName);

dispose를 호출하지 않으면 타이머로 실행되는 ServicePointManager.MaxServicePointIdleTime이 http 연결을 닫습니다. 기본값은 100 초입니다.

ServicePointManager.cs

internal static readonly TimerThread.Callback s_IdleServicePointTimeoutDelegate = new TimerThread.Callback(ServicePointManager.IdleServicePointTimeoutCallback);
private static volatile TimerThread.Queue s_ServicePointIdlingQueue = TimerThread.GetOrCreateQueue(100000);

private static void IdleServicePointTimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context)
{
  ServicePoint servicePoint = (ServicePoint) context;
  if (Logging.On)
    Logging.PrintInfo(Logging.Web, SR.GetString("net_log_closed_idle", (object) "ServicePoint", (object) servicePoint.GetHashCode()));
  lock (ServicePointManager.s_ServicePointTable)
    ServicePointManager.s_ServicePointTable.Remove((object) servicePoint.LookupString);
  servicePoint.ReleaseAllConnectionGroups();
}

유휴 시간을 무한으로 설정하지 않은 경우 dispose를 호출하지 않고 유휴 연결 타이머를 시작하고 연결을 닫는 것이 안전하지만 using 문에서 dispose를 호출하는 것이 좋습니다. HttpClient 인스턴스를 사용하여 리소스를 더 빨리 확보 할 수 있습니다.


답변

짧은 답변 : 아니요. 현재 승인 된 답변의 내용이 정확하지 않습니다. . “일반적인 의견은”HttpClient를 폐기 할 필요가 없다는 것입니다. “

긴 대답 : 다음 진술 모두 사실이며 동시에 달성 가능합니다.

  1. 공식 문서 에서 인용 한 “HttpClient는 한 번 인스턴스화되어 응용 프로그램 수명 동안 재사용됩니다” .”
  2. IDisposable개체를 배치 할 것을 권장합니다 / 가정된다.

그리고 그들은 반드시 서로 충돌하지 않습니다. 재사용을 위해 코드를 구성하는 방법에 관한 문제입니다.HttpClientAND 하고 올바르게 처리 .

다른 답변 에서 인용 한 더 답변 :

사람들을 볼 수있는 우연이 아닌 일부 블로그 게시물 어떻게 비난 HttpClientIDisposable그들을를 사용하는 경향이 인터페이스를 만든다using (var client = new HttpClient()) {...} 패턴을 다음 소진 소켓 핸들러 문제로 이어집니다.

나는 그 무언으로 내려 오는 생각의 개념을 (잘못?)
의 “는 IDisposable 개체가 단명 할 것으로 예상된다” .

그러나이 스타일로 코드를 작성할 때 확실히 수명이 짧은 것처럼 보입니다.

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

IDisposable에 대한 공식 문서
IDisposable객체의 수명이 짧아야 한다고 언급하지 않습니다 . 정의에 따르면 IDisposable은 관리되지 않는 리소스를 해제 할 수있는 메커니즘 일뿐입니다. 더 이상 없습니다. 그런 의미에서 결국 폐기를 촉발 할 것으로 예상되지만 단기적으로 그렇게 할 필요는 없습니다.

따라서 실제 물체의 수명주기 요구 사항에 따라 폐기시기를 적절하게 선택하는 것이 귀하의 임무입니다. IDisposable을 오래 사용하는 것을 막을 수있는 것은 없습니다 :

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

이 새로운 이해를 통해 이제 블로그 게시물 을 다시 방문 하면 “수정”이 HttpClient한 번 초기화 되지만 절대 폐기하지 않는다는 것을 분명히 알 수 있습니다. 따라서 netstat 출력에서 ​​연결이 ESTABLISHED 상태로 유지된다는 것을 알 수 있습니다. 제대로 닫히지 않았습니다. 닫히면 상태는 TIME_WAIT입니다. 실제로 전체 프로그램이 종료 된 후 하나의 연결 만 열면 크게 문제가되지 않으며 블로그 포스터는 여전히 수정 후에도 성능이 향상됩니다. 그러나 여전히 IDisposable을 비난하고 처분하지 않기로 선택하는 것은 개념적으로 올바르지 않습니다.


답변

아직 아무도 언급하지 않은 것 같으므로 .Net Core 2.1에서 HttpClient 및 HttpClientHandler를 관리하는 새로운 최선의 방법은 HttpClientFactory를 사용하는 것입니다. 입니다.

앞에서 언급 한 대부분의 문제와 문제를 깨끗하고 사용하기 쉬운 방식으로 해결합니다. 에서 스티브 고든의 위대한 블로그 게시물 :

.Net Core (2.1.1 이상) 프로젝트에 다음 패키지를 추가하십시오.

Microsoft.AspNetCore.All
Microsoft.Extensions.Http

이것을 Startup.cs에 추가하십시오.

services.AddHttpClient();

주사 및 사용 :

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ValuesController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet]
    public async Task<ActionResult> Get()
    {
        var client = _httpClientFactory.CreateClient();
        var result = await client.GetStringAsync("http://www.google.com");
        return Ok(result);
    }
}

더 많은 기능에 대해서는 Steve의 블로그에서 일련의 게시물을 살펴보십시오.


답변

필자의 경우 실제로 서비스 호출을 수행 한 메소드 내에 HttpClient를 작성 중이었습니다. 다음과 같은 것 :

public void DoServiceCall() {
  var client = new HttpClient();
  await client.PostAsync();
}

Azure 작업자 역할에서 HttpClient를 삭제하지 않고이 메서드를 반복적으로 호출하면 결국 실패합니다. SocketException (연결 시도 실패).

HttpClient를 인스턴스 변수로 만들고 (클래스 수준에서 배치) 문제가 사라졌습니다. 그래서, 안전하다고 가정하면 (당신은 뛰어난 비동기 호출이 없다고 가정) HttpClient를 처분하십시오.