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


57

最近、私はasp.netモンスターからこのブログ投稿に出くわしHttpClientました。以下の方法での使用に関する問題について話します

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すべてのリクエストに対して新しい単一のインスタンスを作成する必要がありますか?静的インスタンスを使用する際の落とし穴はありますか?


使用方法に起因する問題に遭遇しましたか?
-whatsisname

たぶん、この答えこれもチェックしてください。
ジョン・ウー

@whatsisnameいいえ私は持っていませんが、ブログを見て、私はいつもこの間違った使い方をしているのではないかと感じました。したがって、仲間の開発者がいずれかのアプローチで問題を見つけた場合、彼らから理解したかった。
アンキットビジェイ

3
私はそれを自分で試したことはありませんので(答えとしてこれを提供していません)、. NET Core 2.1の時点でマイクロソフトによると、docs.microsoft.com / en-us / dotnet / standard /で
ジョエリ・セブレヒト

(私の答えで述べたように、もっと見やすくしたかったので、短いコメントを書いています。)静的インスタンスはClose()、新しいを行うか開始すると、tcp接続を閉じるハンドシェイクを適切に処理しますGet()。クライアントを使い終わった後に破棄する場合、そのクローズハンドシェイクを処理する人はいません。そのため、ポートはすべてTIME_WAIT状態になります。
Mladen B.

回答:


39

魅力的なブログ投稿のようです。ただし、決定を下す前に、ブログ作成者が実行したのと同じテストを、独自のコードで実行します。また、HttpClientとその動作についてもう少し調べてみます。

この投稿の状態:

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

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

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

参照資料


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


1
これは非常に充実したリストです。これは私の週末の読書です。
アンキットビジェイ

「掘れば、この問題に対処する他のリソースがいくつか見つかります...」というのは、TCP接続の未解決の問題だということですか?
アンキットビジェイ

簡単な答え:静的なHttpClientを使用します。(Webサーバーまたは他のサーバーの)DNSの変更をサポートする必要がある場合、タイムアウト設定について心配する必要があります。
ジェス

3
これは、HttpClientの使い方が@AnkitVijayがコメントしているように「週末の読み取り」であることの台無しです。
USR

DNSの変更以外の@Jess-すべてのクライアントのトラフィックを1つのソケットに投げると、負荷分散も台無しになりますか?
イアン

16

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

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

つまり、HttpClientの再利用が意図されて おり、そうすることが重要である場合、そのような支持者は、多くの「高度なトピック」、「パフォーマンス(アンチ)パターン」、または他のブログ投稿に隠されるのではなく、独自のAPIドキュメントに文書化される方がよい。さもなければ、新しい学習者は手遅れになる前にどのようにそれを知ることになっていますか?

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

私はどちらかの正しいドキュメントページを見つけられませんでした、このトピックに新しいだった多くの開発者を疑う、この知識が広く普及していない理由です、そして、彼らはそれを見つけたとき、人々は驚いた 後に、おそらく、ハードな方法で

2.(誤?)の概念 using IDisposable

これは少しトピックから外れていますが、指摘する価値はありますが、前述のブログ投稿で、インターフェイスがパターンを使用する傾向があり、問題につながる傾向があると非難するHttpClientのは偶然ではありません。IDisposableusing (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を呼び出す必要があります...アプリがリソースをリークしないようにする」ことに反します。

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

「グローバル変数は良いアイデアだと思う人は何人いますか?誰もいません。

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

何が得られますか?シングルトンはグローバル変数の集まりです。」

-この刺激的な講演「グローバルステートアンドシングルトン」からの引用

PS:SqlConnection

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

違いは、実装アプローチが原因です。各HttpClientインスタンスは、独自の接続プール(ここから引用 )を使用します。しかし、SqlConnectionオブジェクト自体はによると、中央の接続プールによって管理され、この

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


14

いくつかのテストで、staticのパフォーマンスの改善を確認しました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ために静的を使用する代わりに静的を使用することで、パフォーマンスが40%から60%向上することを発見しましたHttpClient。パフォーマンステストの結果の詳細は、こちらのブログ投稿に記載しています


1

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

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

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


1

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

-5

HttpClientクラスを使用する方法はありません。重要なのは、環境と制約に合った方法でアプリケーションを設計することです。

HTTPは、パブリックAPIを公開する必要がある場合に使用する優れたプロトコルです。また、軽量で低遅延の内部サービスにも効果的に使用できますが、多くの場合、RPCメッセージキューパターンは内部サービスに適しています。

HTTPを適切に実行することは非常に複雑です。

以下を考慮してください。

  1. ソケットを作成してTCP接続を確立するには、ネットワーク帯域幅と時間を使用します。
  2. HTTP / 1.1は、同じソケットでパイプライン要求をサポートします。前の応答を待つ必要なく、次々に複数のリクエストを送信します-これはおそらく、ブログ投稿で報告された速度の改善に責任があります。
  3. キャッシングとロードバランサー-サーバーの前にロードバランサーがある場合、リクエストに適切なキャッシュヘッダーがあることを確認すると、サーバーの負荷を軽減し、クライアントへの応答を迅速に取得できます。
  4. リソースをポーリングしないで、HTTPチャンクを使用して定期的な応答を返します。

しかし何よりも、テスト、測定、確認を行います。設計どおりに動作しない場合は、期待される結果を達成する方法に関する特定の質問に答えることができます。


4
これは実際に質問に答えることはありません。
-whatsisname

あなたは一つの正しい方法があると仮定しているようです。あるとは思わない。適切な方法でそれを使用し、それがどのように動作するかをテストおよび測定し、満足するまでアプローチを調整する必要があることを知っています。
マイケルショー

HTTPを使用するかどうかを通信に使用するかどうかについて少し書きました。OPは、特定のライブラリコンポーネントの最適な使用方法について質問しました。
-whatsisname

1
@MichaelShaw:をHttpClient実装しIDisposableます。したがって、using必要なたびにステートメントをラップするのに適した、それ自体をクリーンアップする方法を知っている短命のオブジェクトであることを期待するのは不合理ではありません。残念ながら、それは実際の仕組みではありません。OPがリンクしたブログ投稿は、usingステートメントが範囲外になり、HttpClientオブジェクトがおそらく破棄された後も長い間存在するリソース(具体的には、TCPソケット接続)があることを明確に示しています。
ロバートハーベイ

1
私はその思考プロセスを理解しています。アーキテクチャの観点からHTTPについて考えていて、同じサービスに対して多くのリクエストを行うつもりだった場合、キャッシュとパイプライン化について考えていて、HttpClientを短命オブジェクトにすることを考えていたでしょう単に間違っていると感じます。同様に、異なるサーバーにリクエストを行っており、ソケットを有効にしておく利点がない場合は、使用後にHttpClientオブジェクトを破棄するのが理にかなっています。
マイケルショー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.