1.HttpClient クラス

HTTP 要求を送信し、HTTP 応答を受信するためにC#では「System.Net.Http.HttpClient」クラスが提供されています。

HttpClientクラスの利用についてサンプルを作成してみます。

以下のコマンドで新規コンソールアプリを作成します。

コンソールアプリ作成
 dotnet new console -n HttpClientSample
 cd HttpClientSample

次にProgram.csを以下のように変更します。

HttpClientでHttpのGetメソッドの非同期呼出し
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // HttpClientのインスタンスをStaticで生成しています。
    static readonly HttpClient httpClient = new HttpClient();

    static async Task Main(string[] args)
    {
        try
        {
            // 非同期でHttpのGetメソッドをパラメータのURLにリクエストします。
            var response = await httpClient.GetAsync("https://blog.hawkspect.com/"); // URLは適切なものに変更してください
            // HTTPレスポンスからBody部分を取得しています。
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex}");
        }
    }
}

次のコマンドでビルド&実行させるとHTTPレスポンスのボディ部分が取得されます

ビルド&実行
dotnet build

dotnet run

Postメソッドなどの場合は以下の様に変更することで要求できます。

HttpClientでHttpのGetメソッドの非同期呼出し
// 送信するデータをJSON形式で作成
var jsonData = "{\"name\": \"Taro\", \"age\": 30}";

// JSONデータをHttpContentとして準備
var postContent = new StringContent(jsonData, Encoding.UTF8, "application/json");

// POSTリクエストを送信。第一パラメータにはPOSTしたいURLを指定
var response = await httpClient.PostAsync("https://********* */", postContent); // URLは適切なものに変更してください

// レスポンスの内容を読み込む
var content = await response.Content.ReadAsStringAsync();

// レスポンス内容をコンソールに出力
Console.WriteLine(content);

2.注意点

「HttpClientクラスは1度インスタンスした後は、再利用されることを目的としています。 すべての要求に対してHttpClientクラスをインスタンス化すると、大量の負荷の下で使用可能なソケットの数が使い果たされます。」と記載されています

https://learn.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient.-ctor?view=netframework-4.8

実際にどのような場合にソケットの数が使い果たされるか確認してみます。

以下の「ポートの枯渇に関する問題のトラブルシューティング」記載されている内容を確認してみます。

正常な終了またはセッションの突然の終了の後、4 分 (既定値) の期間が経過すると、プロセスまたはアプリケーションによって使用されるポートが使用可能なプールに解放されます。 この 4 分間、TCP 接続状態はTIME_WAIT状態になります。 ポートの枯渇が疑われる状況では、アプリケーションまたはプロセスは、使用したすべてのポートを解放できず、TIME_WAIT状態のままです。

TCPの接続状態と内容

TCP接続状態内容
ESTABLISHED接続が確立され、データの送受信が可能な状態
LISTENINGサーバが接続要求を待機している状態。クライアントからの接続を受け入れる準備ができています
SYN_SENTクライアントが接続要求を送信し、サーバからの応答(SYN-ACK)を待っている状態
SYN_RECEIVEDサーバがクライアントからの接続要求を受け取り、応答を送信したが、クライアントからのACKを待っている状態
FIN_WAIT_1一方のホストが接続終了を通知するためにFINパケットを送信した状態
FIN_WAIT_2相手からのACKを受け取った後の状態。相手が接続を終了するのを待っています 
CLOSE_WAIT相手側からのFINを受け取った状態。接続を終了するために自分からもFINを送信する必要があります
LAST_ACK自分がFINを送った後、相手からのACKを待っている状態
TIME_WAIT接続が正常に終了した後、しばらくの間保持される状態。遅延パケットの影響を防ぎます
CLOSED接続が完全に終了し、リソースが解放された状態

以下のコマンドで、tcpの動的ポートの範囲を確認できます。 デフォルトで開始ポートが49152でポート数16384が利用可能です。

プロトコル tcp の動的ポートの範囲
netsh int ipv4 show dynamicport tcp

プロトコル tcp の動的ポートの範囲
---------------------------------
開始ポート      : 49152
ポート数        : 16384

今回は、事象が発生しやすいように開始ポートを10000でポート数255で設定してポートの枯渇が発生するかを確認していきます。

開始ポートを10000でポート数255
netsh int ipv4 set dynamicport tcp start=10000 num=255

TIME_WAIT状態のTCP接続がいくつあるかをカウントします。
netstat -an | find /c "TIME_WAIT"

HttpClientの利用で重要なのは、インスタンスが繰り返し作成されたり破棄されたりしないことです。

以下の様にusingブロック内でオブジェクトを作成すると、 そのオブジェクトの使用が終了した時点で自動的にリソースが解放されます。 これはIDisposableインターフェースを実装しているクラスに対して有効ですが HttpClientでの利用はリソースが解放されますが、 TCP接続状態はTIME_WAIT状態になりポートの解放が行われずWindows最大ソケット数制限に引っ掛かります。

HttpClientのusing利用
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // countにHttpClientのインスタンス生成と破棄を繰り返す処理
        int count = 300; 

        try
        {
            for (var i = 1; i <= count; i++)
            {
                // usingブロックでHttpClientのインスタンス生成と破棄
                using (var httpClient = new HttpClient())
                {
                    var response = await httpClient.GetAsync("https://****/"); // URLは適切なものに変更してください
                    var content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine(content);
                } // ここでhttpClientがDisposeされる
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex}");
        }
    }
}

上記アプリケーション実行ではTCP接続状態はTIME_WAIT状態になりポートの解放が行われずTIME_WAITの件数が増えていきます。 4分経過を待たないと解放されないためTIME_WAIT状態のポートが大量に発生するとポートが枯渇してしまいます。

ビルド&実行&TIME_WAITの件数を確認
dotnet build

dotnet run

netstat -an | find /c "TIME_WAIT"

いずれWindows最大ソケット数制限に引っ掛かり以下のようなエラーが発生します。

例外内容
#元の例外
InnerException  {"通常、各ソケット アドレスに対してプロトコル、ネットワーク アドレス、またはポートのどれか 1 つのみを使用できます。 .... "}   System.Exception {System.Net.Sockets.SocketException}

#例外
ex  {"この要求の送信中にエラーが発生しました。"}    System.Exception {System.Net.Http.HttpRequestException}

#メッセージ
Message "リモート サーバーに接続できません。"

上記のループ内で、staticやシングルトンで定義したHttpClientを利用するが場合は、 リソースを使いまわすため上記のようにTCP接続状態を次々と生成してポート解放が行われずTIME_WAITになるようなことがないことが確認できます。

HttpClientのstatic利用
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // HttpClientのインスタンスを静的に生成
    static readonly HttpClient httpClient = new HttpClient();

    static async Task Main(string[] args)
    {
        // countにリクエストの回数を設定
        int count = 300;

        try
        {
            for (var i = 1; i <= count; i++)
            {
                var response = await httpClient.GetAsync("https://****"); // URLは適切なものに変更してください
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine(content);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

3.まとめ

今回はHttpClientクラスの簡単な利用方法と注意点を記載しましたが、 その他にもエラーハンドリングやタイムアウトなど考慮が必要な要素もあります。 以下のガイドラインに記載されたTCP ポートは接続が閉じられてもすぐには解放されないことを検証した内容となります。

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/networking/http/httpclient-guidelines

今後は.NET FrameworkのC#でIHttpClientFactoryの利用なども確認していきたいと思います。

本コンテンツはプロモーションが含まれます。