lycheejam's tech log

チラ裏のメモ帳 | プログラミングは苦手、インフラが得意なつもり。

【実験】C# HttpClientのソケット大量消費の回避

概要

ちょっとしたものを作っている過程でHttpClientを使用した場合のソケット数について気になることがあったので
netstatコマンドで計測しながら下記の2パターン実験しました。
公式ドキュメントに記載のあるSocketExceptionエラーを回避する方法ですが自分で確かめたかった次第です。

  • HttpClientusingで囲んで使用するパターン(Badパターン)
  • HttpClientを静的メンバとして宣言しインスタンス使い回すパターン(Goodパターン)

正味、コード的には公式ドキュメントのまんまですがnetstatでの計測結果付きです。

HttpClientFactoryについては触れないのであしからず。

あと検証環境はMacOSです。WindowsMacOSで中でラップしてるクラスが違うっぽいので動作の違いに注意してください。

目次

参考サイト様

環境

成果物

HttpClientを使用したソケット消費数の計測

消費と言う言葉が適当かはわかりませんが消費と言わせてもらいます。

通常、System.Net.Http名前空間HttpClient Classを使用する場合
IDisposableが継承されているためusing句で囲って使用する様に見えますがこれでは一度開いたソケットが閉じられません。
※正しくはソケットをクローズするまでに時間がかかりメソッドの処理が終了してもソケットが残ったままとなっている現象。

そのため、公式ドキュメントではHttpClientを静的メンバとしてソケットを使い回すように案内されています。
詳細については下記が参考になります。

下記、パターン2種類のサンプルコードと最後に結果をスクショで提示します。

  • HttpClientusingで囲んで使用するパターン(Badパターン)
  • HttpClientを静的メンバとして宣言しインスタンス使い回すパターン(Goodパターン)

実験にはIP固定の静的サイトのほうが都合良かったので自身のtodoアプリのサーバーを間借りしました。

HttpClientusingで囲むパターン(Bad)

HttpClientの実装は下記の通りです。
usingで囲って対象のサイトからHTMLのbodyを取ってきます。

  • HttpClientBadExample.cs
public class HttpClientBadExample {
    public async Task<string> RunAsync (string url) {
        var result = "";
        using (var client = new HttpClient()) {
            var response = await client.GetAsync(url);
            result = await response.Content.ReadAsStringAsync();
        }
        return result;
    }
}

そして、下記がHttpClient呼び出し元です。
forを使用してHttpClientBadExampleクラスのRunAsyncが10回実行されるようにします。

  • Zikken.cs
public class Zikken {
    private readonly string _url;

    public Zikken() {
        _url = "http://todo.kitigai.org/zikken/";
    }

    public async Task BadPatternA () {
        Console.WriteLine("悪い例1");

        var badclient = new HttpClientBadExample();

        for (int i = 0; i < 10; i++) {
            Console.WriteLine("{0}回目:{1}", i, await badclient.RunAsync(_url));
        }
    }

    public async Task BadPatternB () {
        Console.WriteLine("悪い例2");

        for (int i = 0; i < 10; i++) {
            var badclient = new HttpClientBadExample();
            Console.WriteLine("{0}回目:{1}", i, await badclient.RunAsync(_url));
        }
    }
}

現時点の想定ではプログラムの実行が終了してもソケットが10個残ると予想しています。

結果

計測にはnetstatコマンドを使用し、watchコマンドで確認しました。 結果は以下の通りです。

想定通りソケットが10個使用されておりプログラムが終了した状態でも残ったままです。
BadPatternAもBも結果は変わらずです。

実行前 実行後
f:id:HM_Atlas:20190425170818p:plain f:id:HM_Atlas:20190425170822p:plain

簡単にindex.htmlbodyを取ってるだけなので10〜20秒くらいでソケットは閉じられます。

HttpClientを静的メンバとして使い回すパターン(Good)

今度は先程と違いHttpClientを静的メンバとしてインスタンス間で使いまわします。

  • HttpClientGoodExample.cs
public class HttpClientGoodExample {
    private static readonly HttpClient _client = new HttpClient();

    public async Task<string> RunAsync (string url) {
        var response = await _client.GetAsync(url);
        return await response.Content.ReadAsStringAsync();
    }
}

こちらは先程と同様にHttpClientの呼び出し元です。
forを使用してHttpClientGoodExampleクラスのRunAsyncが10回実行されるようにします。

  • Zikken.cs
public class Zikken {
    private readonly string _url;

    public Zikken() {
        _url = "http://todo.kitigai.org/zikken/";
    }
    public async Task GoodPatternA () {
        Console.WriteLine("良い例1");

        var goodclient = new HttpClientGoodExample();

        for (int i = 0; i < 10; i++) {
            Console.WriteLine("{0}回目:{1}", i, await goodclient.RunAsync(_url));
        }
    }

    public async Task GoodPatternB () {
        Console.WriteLine("良い例2");

        for (int i = 0; i < 10; i++) {
            var goodclient = new HttpClientGoodExample();
            Console.WriteLine("{0}回目:{1}", i, await goodclient.RunAsync(_url));
        }
    }
}

このパターンは想定ではインスタンス間でHttpClientを使いまわしているはずなのでソケットは10個よりも少なくなると予想しています。

結果

計測も先程と同様にnetstatwatchコマンドで確認です。

想定通り、Badパターンと違いソケット数が減少しました。
GoodPatternAもBも結果は変わらずです。
※試行回数が少ない+簡単なコードなので1ソケットと言う結果になったと思われ。

実行前 実行後
f:id:HM_Atlas:20190425170818p:plain f:id:HM_Atlas:20190425170824p:plain

結果比較

結果はもう提示してしまっているのでこれ以上ありませんが比較用に貼っておきます。

実行前 usingパターン 静的メンバパターン
f:id:HM_Atlas:20190425170818p:plain f:id:HM_Atlas:20190425170822p:plain f:id:HM_Atlas:20190425170824p:plain

雑感

昨日、コナンの映画見てきたんですが「これが...コナン...?」って感想でした。