by WebSurfer
21. December 2024 14:02
HTTP 1.1 仕様では同時接続数は 2 つまでとなっているそうで (過去の話らしい)、それに準じて .NET Framework 4.8 の HttpWebRequest を使ったアプリでも同時接続数は 2 つまでの制限がかかります。(要求先が localhost では無制限となります。詳しい話は先の記事「HttpWebRequest の localhost への同時接続数」を見てください)
その同時接続数の制限が、この記事を書いた時点での最新ターゲットフレームワーク .NET 9.0 の HttpClient を使ったアプリではどうなるかを調べたというのがこの記事の話です。
実は、後になって気が付いたのですが、Microsoft のドキュメント「System.Net.Http.HttpClient クラス」の「接続のプール」のセクションの「注意」に、
"同時接続の数を制限するには MaxConnectionsPerServer プロパティを設定します。既定では同時 HTTP/1.1 接続の数は無制限です"
・・・と書いてあって、わざわざコードを書いたりして調べるまでもなかったです。でも、せっかく調べたので、調べたことを以下に書いておきます。
上の画像は、自分が使っているホスティングサービスの ASP.NET Web Forms アプリに、要求を受けて 10 秒後に Hello World という文字列を返す HTTP ジェネリックハンドラを作り、それに対して HttpClient を使ったマルチスレッドアプリから同時に 8 つの要求を出し、その応答を表示したものです。(同時要求 8 は検証に使った PC のコア数です。その数まではスレッドプールから一気にスレッドを取得できるそうです)
異なるスレッドで 8 つの要求が同時にサーバーに送信され、10 秒後に 8 つの応答が同時にクライアントに返されています。すなわち同時接続数の制限はされてないという結果でした。
ちなみに、HttpWebRequest のように同時接続数が 2 つまでという制限がかかると、2 を超えた分は前回の応答が返ってきてからでないとクライアントから要求が出ず、2 要求毎にサーバーでの処理時間の 10 秒ずつ待たされ、8 つ要求出した場合は全部終わるのに 40 秒かかります。
以下に検証に使った .NET 9.0 の HttpClient のコンソールアプリのコードを載せておきます。Visual Studio 2022 のテンプレートで作ったものです。
namespace ConsoleAppLocalhost
{
internal class Program
{
static readonly HttpClient client = new();
static async Task Main(string[] args)
{
// localhost
// ローカルの IIS Express で動くASP.NET MVC5 アプリの
// アクションメソッド。
// 要求を受けて 10 秒後に OK という文字列を返す
//var uri = "https://localhost:44365/Home/sample";
// ホスティングサービスの IIS で動く ASP.NET Web Forms
// アプリの HTTP ジェネリックハンドラ。
// 要求を受けて 10 秒後に Hello World という文字列を返す
var uri = "https://example.com/xxxxxxxx";
// websiteproject.com
// ローカル IIS で動く ASP.NET Web Forms アプリの HTTP
// ジェネリックハンドラ。
// hosts ファイルで 127.0.0.1 に websiteproject.com と
// いうホスト名を付けたのでそのホスト名で呼び出せる。
// 要求を受けて 10 秒後に Hello World という文字列を返す
//var uri = "http://websiteproject.com/Sample.ashx";
var tasks = new List<Task>();
// 同じ URL を 5 回同時に要求する
foreach (var i in Enumerable.Range(0, 5))
{
var task = Task.Run(async () =>
{
// ThreadId と開始時刻
int id = Thread.CurrentThread.ManagedThreadId;
string start = $" / ThreadID = {id}, " +
$"start: {DateTime.Now:ss.fff}, ";
using HttpResponseMessage response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// 終了時刻
string end = $"end: {DateTime.Now:ss.fff}";
responseBody += start + end;
Console.WriteLine(responseBody);
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
Console.WriteLine("Finish");
Console.ReadLine();
}
}
}