web-dev-qa-db-ja.com

すべての要求に対してHttpClientの新しい単一インスタンスを作成する必要がありますか?

最近私は遭遇しました このブログ投稿 asp.netモンスターからHttpClientinの使用に関する問題について次のように話しています:

using(var client = new HttpClient())
{
}

ブログの投稿に従って、すべてのリクエストの後にHttpClientを破棄すると、TCP接続を開いたままにすることができます。これにより、System.Net.Sockets.SocketException

投稿による正しい方法は、HttpClientの単一インスタンスを作成することです。これは、ソケットの無駄を減らすのに役立ちます。

投稿から:

HttpClientの単一のインスタンスを共有する場合、ソケットを再利用することで、ソケットの無駄を減らすことができます。

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

HttpClientオブジェクトは、これが最良の使用方法であると感じたため、使用後は常に破棄しました。しかし、このブログの投稿は、今までずっと間違っていたように感じます。

すべてのリクエストに対してHttpClientの新しい単一インスタンスを作成する必要がありますか?静的インスタンスを使用することの落とし穴はありますか?

69
Ankit Vijay

説得力のあるブログ投稿のようです。ただし、決定する前に、まずブログのライターが実行したのと同じテストを、独自のコードで実行します。また、HttpClientとその動作についてもう少し詳しく調べてみます。

この投稿 の状態:

HttpClientインスタンスは、そのインスタンスによって実行されるすべてのリクエストに適用される設定のコレクションです。さらに、すべてのHttpClientインスタンスは独自の接続プールを使用し、そのリクエストを他のHttpClientインスタンスによって実行されるリクエストから分離します。

したがって、HttpClientが共有されているときにおそらく何が起こっているかは、接続が再利用されていることであり、永続的な接続が必要ない場合は問題ありません。これが問題になるかどうかを確実に知る唯一の方法は、独自のパフォーマンステストを実行することです。

掘り下げると、この問題に対処する他のいくつかのリソース(Microsoftベストプラクティスの記事を含む)が見つかりますので、とにかく実装することをお勧めします(いくつかの注意事項があります)。

参考文献

間違ったHttpclientを使用していて、ソフトウェアを不安定化しています
Singleton HttpClient?この深刻な動作とその修正方法に注意してください
Microsoftパターンと実践-パフォーマンスの最適化:不適切なインスタンス化
コードレビューで再利用可能なHttpClientの単一インスタンス
Singleton HttpClientはDNSの変更を考慮しません(CoreFX)
HttpClientを使用するための一般的なアドバイス

43
Robert Harvey

私はパーティーに遅れましたが、これはこのトリッキーなトピックに関する私の学習の旅です。

1. HttpClientの再利用に関する公式の支持者はどこにいますか?

つまり、 HttpClientの再利用が意図されている および そうすることが重要 の場合、そのような擁護者は、多くの「高度なトピック」に隠されているのではなく、独自のAPIドキュメントに文書化されている、「Performance(anti)pattern」またはその他のブログ投稿。そうでなければ、新しい学習者は手遅れになる前にそれを知ることになっているのでしょうか?

現在(2018年5月)の時点で、「c#httpclient」をグーグルするときの最初の検索結果は MSDNのこのAPIリファレンスページ を指していますが、その意図はまったく言及されていません。まあ、初心者のためのここでのレッスン1は、MSDNヘルプページの見出しの直後にある[その他のバージョン]リンクをクリックすることです。おそらく、そこに「現在のバージョン」へのリンクがあります。このHttpClientの場合、最新のドキュメント ここにはその意図の説明が含まれています が表示されます。

このトピックに不慣れな多くの開発者も正しいドキュメントページを見つけられなかったのではないかと思います。そのため、この知識は広く普及しておらず、人々はそれを知ったとき驚きました 後で 、おそらく 難しい方法で

2. usingIDisposableの(誤った)概念

これは少し話題から外れていますが、それでも指摘する価値があります。前述のブログ投稿で、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になります。実際には、プログラム全体が終了した後に開いている接続を1つだけリークすることは大した問題ではなく、ブログの投稿者は修正後もパフォーマンスが向上しています。しかし、それでも、IDisposableを非難し、それを破棄しないことを選択することは概念的に正しくありません。

3. HttpClientを静的プロパティに配置する必要がありますか、それともシングルトンとして配置する必要がありますか?

前のセクションの理解に基づいて、私はここでの答えが明らかになると思います:「必ずしもではない」。 HttpClientを再利用し、最終的には(理想的には)コードを破棄する限り、コードをどのように編成するかによります。

陽気に、 現在の公式文書の備考セクション の例でさえ、それは厳密に正しいです。これは、破棄されない静的HttpClientプロパティを含む「GoodController」クラスを定義します。 「例」セクションの別の例 が強調することに従わない:「disposeを呼び出す必要があります...したがって、アプリはリソースをリークしません」。

そして最後に、シングルトンには独自の課題がないわけではありません。

「グローバル変数は良い考えだと思う人はどれくらいいるだろうか。誰もいない。

シングルトンが良いアイデアだと思う人は何人いますか?いくつか。

何ができますか?シングルトンは、単なるグローバル変数の集まりです。」

-この刺激的な講演から引用 "Global State and Singletons"

PS:SqlConnection

これは現在のQ&Aとは無関係ですが、おそらく知っておくとよいでしょう。 SqlConnectionの使用パターンは異なります。 SqlConnectionを再利用する必要はありません のほうが接続プールを適切に処理するためです。

違いは、実装方法によるものです。各HttpClientインスタンスは独自の接続プールを使用します( here から引用)。しかし、SqlConnection自体は、 this に従って中央の接続プールによって管理されます。

また、HttpClientの場合と同じように、SqlConnectionを破棄する必要があります。

16
RayLuo

いくつかのテストで、静的HttpClientを使用するとパフォーマンスが向上することがわかりました。テストには以下のコードを使用しました。

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

テスト用:

  • 10、100、1000、1000接続でコードを実行しました。
  • 各テストを3回実行して、平均を確認します。
  • 一度に1つのメソッドを実行

HttpClientリクエスト用に廃棄するのではなく、静的HttpClientを使用すると、40%から60%uonのパフォーマンスの向上が見つかりました。パフォーマンステスト結果の詳細はブログの投稿 here に記載しています。

15
Ankit Vijay

以下は、HttpClientとHttpClientHandlerを効率的に使用する基本的なAPIクライアントです。リクエストを作成するために新しいHttpClientを作成すると、多くのオーバーヘッドが発生します。リクエストごとにHttpClientを再作成しないでください。可能な限りHttpClientを再利用する...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

使用法:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
1
Alper Ebicoglu

適切に TCP接続 を閉じるには、FIN-FIN + ACK-ACKパケットシーケンス(SYN-SYN + ACK-ACKと同様に、 opening a TCP connection )。Close()メソッドを呼び出すだけの場合(通常は HttpClient が破棄されているときに発生します)、そしてリモート側が(FIN + ACKを使用して)クローズ要求を確認するのを待たないでください。リスナー(HttpClient)を破棄したため、ローカルTCP=ポートでTIME_WAIT状態になります。また、リモートピアがFIN + ACKパケットを送信すると、ポートの状態を適切なクローズ状態にリセットする機会がありませんでした。

TCP接続を閉じる適切な方法は、.Close()メソッドを呼び出し、反対側からのクローズイベント(FIN + ACK)が私たちの側に到着するのを待つことです。最終的なACKを送信して、HttpClientを破棄できます。

追加するだけで、 "Connection:Keep-Alive" HTTPヘッダーがあるため、HTTPリクエストを実行している場合、TCP接続を開いたままにしておくことは理にかなっています。さらに、リモート代わりに、HTTPヘッダー "Connection:Close"を設定することにより、ピアが接続を閉じるため、ローカルポートは、TIME_WAIT状態ではなく、常に適切に閉じられます。

1
Mladen B.