web-dev-qa-db-ja.com

HttpClientとHttpClientHandlerを廃棄する必要がありますか?

.NET Framework 4.5では、 System.Net.Http.HttpClient および System.Net.Http.HttpClientHandler が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()を呼び出していません。例えば:

announcement のコメントで、誰かがMicrosoftの従業員に尋ねました。

サンプルを確認したところ、HttpClientインスタンスに対して破棄アクションを実行しなかったことがわかりました。私は自分のアプリでusingステートメントを使用してHttpClientのすべてのインスタンスを使用しましたが、HttpClientがIDisposableインターフェイスを実装しているので、それが正しい方法だと思いました。私は正しい道を進んでいますか?

彼の答えは:

一般的にそれは正しいですが、 "using"と "as 4"を混同しないように注意しなければなりません。Net4.5では、 "using"ステートメント内で "await"を使用できます。

ところで、同じHttpClientを好きなときに何度でも再利用できるので、通常は作成/破棄しないでください。

2番目の段落はこの質問には不要です。これは、HttpClientインスタンスを何回使用できるかということではなく、不要になった後に破棄する必要があるかどうかについては関係ありません。

(更新:実際には、2番目の段落が@DPedenによって以下に提供されるように、答えへの鍵です。)

だから私の質問は次のとおりです。

  1. 現在の実装(.NET Framework 4.5)では、HttpClientおよびHttpClientHandlerインスタンスでDispose()を呼び出す必要がありますか?明確化:「必要」とは、リソースの漏洩やデータ破損のリスクなど、廃棄しないことによる悪影響があるかどうかを意味します。

  2. それが必要でないならば、それは彼らがIDisposableを実装するので、とにかく「良い習慣」であろうか?

  3. それが必要な場合(または推奨される場合)、 このコードは で安全に実装されていますか(.NET Framework 4.5の場合)。

  4. これらのクラスがDispose()の呼び出しを必要としない場合、なぜそれらはIDisposableとして実装されているのでしょうか。

  5. それらが必要な場合、またはそれが推奨される方法である場合、Microsoftの例は誤解を招くか、または安全ではありませんか?

290

一般的なコンセンサスは、あなたがHttpClientを処分する必要はない(するべきではない)ということです。

それが働く方法に深く関わっている多くの人々はこれを述べました。

参考のために、 Darrel Millerのブログ投稿 および関連するSO post: HttpClientクロールの結果、メモリリークが発生します

また、ASP.NETを使用したEvolvable Web APIの設計からのHttpClientの章を読むこと をお勧めします フードの下で何が起こっているかについての文脈のために、特にここで引用された「ライフサイクル」セクション:

HttpClientはIDisposableインターフェイスを間接的に実装していますが、HttpClientの標準的な使い方は、リクエストごとにそれを破棄することではありません。 HttpClientオブジェクトは、アプリケーションがHTTPリクエストを送信する必要がある限り存続することを目的としています。オブジェクトを複数のリクエストにまたがって存在させることで、DefaultRequestHeadersを設定する場所が有効になり、HttpWebRequestで必要とされたリクエストごとにCredentialCacheやCookieContainerなどを再指定する必要がなくなります。

あるいはDotPeekを開くこともできます。

226
David Peden

現在の答えは少し混乱し誤解を招きやすいものであり、DNSの重要な意味合いが欠けています。私は物事が明確に立つ場所を要約しようとします。

  1. 一般的に言えば、ほとんどのIDisposableオブジェクトは、それらの処理が完了したら、特に 独自の名前付き/共有OSリソースHttpClientも例外ではありません。これは Darrel Miller がキャンセルトークンを割り当て、要求/応答の本文をアンマネージストリームにできるためです。
  2. ただし、 HttpClientのベストプラクティス では、1つのインスタンスを作成し、可能な限り再利用する必要があります(マルチスレッドシナリオでは thread-safeメンバー を使用)。したがって、ほとんどのシナリオでは、常に必要になるからといって破棄することはありません
  3. 同じHttpClientを「永久に」再利用する場合の問題は、 DNSの変更に関係なく、元のDNS解決されたIPに対して基本的なHTTP接続が開いたままになる可能性がある です。これは、ブルー/グリーン展開やDNSベースのフェイルオーバーなどのシナリオで問題になる可能性があります。この問題に対処するためのさまざまなアプローチがあります。DNSの変更が行われた後にサーバーがConnection:closeヘッダーを送信することを含む最も信頼できるアプローチです。別の可能性としては、クライアント側でHttpClientを定期的に、またはDNSの変更について学習する何らかのメカニズムを介してリサイクルすることがあります。詳細については https://github.com/dotnet/corefx/issues/11224 を参照してください(リンク先のブログ投稿で提案されているコードをやみくもに使用する前に注意深く読むことをお勧めします)。
39
Ohad Schneider

私の理解では、Dispose()の呼び出しは、後で必要なリソースをロックするときにのみ必要です(特定の接続など)。使用しなくなったリソースを解放することが常に推奨されます。たとえそれらが再び必要なくなったとしても、単純にを使用するべきではありません。あなたが使っていないリソースを握っている(意図された通り)。

マイクロソフトの例は、必ずしも間違いではありません。アプリケーションが終了すると、使用されているすべてのリソースが解放されます。そしてその例の場合、それはHttpClientが使われ終わった直後に起こります。同様のケースで、Dispose()を明示的に呼び出すことはやや不必要です。

しかし、一般的に、クラスがIDisposableを実装するとき、完全に準備ができてすぐに使えるようになるとすぐに、そのインスタンスのDispose()を使用する必要があるということが理解されます。私は、これがHttpClientのような場合に特に当てはまると思います。そこでは、リソースまたは接続が/ openに保持されているかどうかに関して明確に文書化されていません。接続が再び[再利用]される場合、それをDipose()ingする必要はありません。その場合、「完全に準備ができている」わけではありません。

IDisposable.Disposeメソッド および Disposeを呼び出すタイミング も参照してください。

16
svidgen

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を呼び出さずにアイドル接続タイマーを起動させて接続を閉じるのが安全です。 HttpClientインスタンスを使い終え、リソースをより早く解放することができます。

8

私の場合は、実際にサービス呼び出しを行ったメソッド内にHttpClientを作成していました。何かのようなもの:

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

Azureワーカーロールでは、(HttpClientを破棄せずに)このメソッドを繰り返し呼び出した後、最終的にSocketExceptionで失敗します(接続の試行に失敗しました)。

私はHttpClientをインスタンス変数にし(クラスレベルでそれを処理します)、問題は解決しました。そのため、HttpClientを安全に(未処理の非同期呼び出しがない)と仮定して、破棄します。

5
David Faivre

簡単な答え:いいえ、現在受け入れられている答えの文は正確ではありません: "一般的なコンセンサスはあなたが処分する必要はないということですHttpClientの "#:。

長めの答え:次の二つのことが当てはまります。

  1. official documentation から引用した「HttpClientは一度インスタンス化され、アプリケーションの存続期間を通して再利用されることを意図しています」。
  2. IDisposableオブジェクトは破棄されることになっています。

そして、彼らはお互いに必然的に矛盾することはありません。 HttpClientを再利用するためにコードをどのように編成し、それでも適切に配置するかは、問題です。

偶数長い答えを私の から引用した別の答え

のブログ記事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();
        }
    }
}

この新しい理解により、今度は というそのブログ投稿 をもう一度見てください。 "fix"はHttpClientを一度初期化しますが、決して破棄しないことがはっきりわかります。そのnetstatは、接続がESTABLISHED状態のままであることを出力しました。これは、接続が正しく閉じられていないことを意味します。閉じられていた場合、その状態は代わりにTIME_WAITになります。実際には、プログラム全体が終了した後に開いている接続を1つだけリークすることは大したことではありません。ブログのポスターには、修正後も依然としてパフォーマンスの向上が見られます。しかし、それでも、IDisposableを非難してそれを破棄しないことを選択することは概念的に正しくありません。

4
RayLuo

一般的な使用法(レスポンス<2GB)では、HttpResponseMessagesを破棄する必要はありません。

HttpClientメソッドの戻り値の型は、それらのストリームコンテンツが完全に読み取られていない場合は破棄する必要があります。そうでなければ、それらがガベージコレクションされるまでCLRがそれらのストリームを閉じることができることを知る方法はありません。

  • データをbyte [](例:GetByteArrayAsync)または文字列に読み込む場合は、すべてのデータが読み込まれるため、破棄する必要はありません。
  • 他のオーバーロードはデフォルトで最大2GBまでのストリームの読み込みになります(HttpCompletionOptionはResponseContentRead、HttpClient.MaxResponseContentBufferSizeのデフォルトは2GBです)

HttpCompletionOptionをResponseHeadersReadに設定した場合、またはレスポンスが2GBより大きい場合は、クリーンアップする必要があります。これは、HttpResponseMessageに対してDisposeを呼び出すか、HttpResonseMessageコンテンツから取得したストリームに対してDispose/Closeを呼び出すか、コンテンツを完全に読み取ることで実行できます。

HttpClientでDisposeを呼び出すかどうかは、保留中の要求をキャンセルするかどうかによって異なります。

3
Tom Deseyn

HttpClientを破棄したい場合は、リソースプールとして設定すれば可能です。そしてあなたのアプリケーションの終わりに、あなたはあなたのリソースプールを破棄します。

コード:

// Notice that IDisposable is not implemented here!
public interface HttpClientHandle
{
    HttpRequestHeaders DefaultRequestHeaders { get; }
    Uri BaseAddress { get; set; }
    // ...
    // All the other methods from peeking at HttpClient
}

public class HttpClientHander : HttpClient, HttpClientHandle, IDisposable
{
    public static ConditionalWeakTable<Uri, HttpClientHander> _httpClientsPool;
    public static HashSet<Uri> _uris;

    static HttpClientHander()
    {
        _httpClientsPool = new ConditionalWeakTable<Uri, HttpClientHander>();
        _uris = new HashSet<Uri>();
        SetupGlobalPoolFinalizer();
    }

    private DateTime _delayFinalization = DateTime.MinValue;
    private bool _isDisposed = false;

    public static HttpClientHandle GetHttpClientHandle(Uri baseUrl)
    {
        HttpClientHander httpClient = _httpClientsPool.GetOrCreateValue(baseUrl);
        _uris.Add(baseUrl);
        httpClient._delayFinalization = DateTime.MinValue;
        httpClient.BaseAddress = baseUrl;

        return httpClient;
    }

    void IDisposable.Dispose()
    {
        _isDisposed = true;
        GC.SuppressFinalize(this);

        base.Dispose();
    }

    ~HttpClientHander()
    {
        if (_delayFinalization == DateTime.MinValue)
            _delayFinalization = DateTime.UtcNow;
        if (DateTime.UtcNow.Subtract(_delayFinalization) < base.Timeout)
            GC.ReRegisterForFinalize(this);
    }

    private static void SetupGlobalPoolFinalizer()
    {
        AppDomain.CurrentDomain.ProcessExit +=
            (sender, eventArgs) => { FinalizeGlobalPool(); };
    }

    private static void FinalizeGlobalPool()
    {
        foreach (var key in _uris)
        {
            HttpClientHander value = null;
            if (_httpClientsPool.TryGetValue(key, out value))
                try { value.Dispose(); } catch { }
        }

        _uris.Clear();
        _httpClientsPool = null;
    }
}

var handler = HttpClientHander.GetHttpClientHandle(新しいUri( "base url")).

  • HttpClientは、インターフェイスとしてDispose()を呼び出すことはできません。
  • Dispose()は、ガベージコレクタによって遅延して呼び出されます。プログラムがデストラクタを介してオブジェクトをクリーンアップするとき。
  • Weak References +遅延クリーンアップロジックを使用するため、頻繁に再利用されている限り使用中です。
  • 渡された各ベースURLに新しいHttpClientを割り当てるだけです。オハドシュナイダーが説明した理由は以下のとおりです。ベースURLを変更したときの動作が悪い。
  • HttpClientHandleはテストでのモッキングを可能にします
2
TamusJRoyce

コンストラクタで依存性注入を使用すると、HttpClientの有効期間の管理が容易になります。有効期間の管理者は、それを必要とするコードの外に置き、後で簡単に変更できるようになります。

私の現在の好みは、ターゲットエンドポイントドメインごとにHttpClientから継承する個別のhttpクライアントクラスを作成し、それから依存性注入を使用してシングルトンにすることです。 public class ExampleHttpClient : HttpClient { ... }

それから、そのAPIにアクセスする必要があるサービスクラスで、カスタムhttpクライアントへのコンストラクター依存関係を取ります。これは寿命の問題を解決し、接続プーリングに関しては利点があります。

あなたは https://stackoverflow.com/a/50238944/314085 に関連する答えでうまくいった例を見ることができます

1
alastairtree

誰もまだここで言及しているようには見えないので、.Net Core 2.1でHttpClientとHttpClientHandlerを管理する新しい最良の方法は HttpClientFactory を使用することです。

前述の問題のほとんどを解決し、使いやすい方法で問題を解決します。 Steve Gordonの素晴らしいブログ投稿

次のパッケージを.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のブログで一連の投稿を調べてください。

1
pcdev

HttpClientはHttpMessageInvokerクラスを継承し、HttpMessageInvokerはIDisposalインターフェイスを継承し、HttpClientHandlerはHttpMessageHandlerクラスを継承し、HttpMessageHandlerはIDisposalインターフェイスを継承するため、Disposeを呼び出す必要はありません。

0