WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

localhost への同時接続数

by WebSurfer 18. June 2021 15:03

.NET Framework を使って Web にアクセスする WinForms などのアプリ開発時に、検証用に自分の開発マシンに Web アプリを作ってアクセスすることがあると思います。その際、localhost への要求の場合は同時接続数の制約が外れることに注意が必要という話を書きます。(検証したのは HttpWebRequest のみで HttpClient は未検証。Core はすべて未検証です)

localhost の場合

(元の話は teratail のスレッド「C#にて並列かつ非同期でWebRequestを使用する方法について」です)

HTTP 1.1 仕様では同時接続は 2 つまでとなっています。HttpWebRequest を使った場合にもその制約が適用されるようですが、(1) localhost への要求の場合で、かつ (2) ユーザーが同時接続数の設定 (ServicePointManager.DefaultConnectionLimit) を変えない場合、Int32.MaxValue になります。

上の画像がその例です。要求先はローカルの IIS Express で動く検証用の ASP.NET Web アプリで、要求を受けて 10 秒後に OK という文字列を返します。その URL を、この記事の下の方に記載したサンプルコードにあるように、マルチスレッドで HttpWebRequest を使って同時に 5 回要求した結果です。end の時間が 5 つとも同じになっているのに注目してください。

同時接続が 2 つまでに制限されていれば、一度にサーバーに要求が出るのは 2 つまでで、3 つ目以降の要求は 10 秒経って先の応答が戻ってこないと出ないので、2 つおきに end の時間が 10 秒ずつ遅れるという結果になるはずです。そうなっていないのは、localhost への要求なので同時接続数の制限が外れたからです (ServicePoint.ConnectionLimit が Int32.MaxValue になっています)。

自分が使っているレンタルサーバー (当然 localhost ではない) で動く ASP.NET Web Forms アプリに、要求を受けて 10 秒後に Hello World という文字列を返す HTTP ジェネリックハンドラを作って、それを下のサンプルコードで同時に 5 回要求した場合は下のようになります。

surferonwww.info の場合

デフォルトの同時接続数 2 は有効になっていて (ServicePoint.ConnectionLimit が 2 になっています)、2 を超えた分は先の応答が返ってきてからでないと要求が出ず、上の画像の end を見ると分かりますが 2 要求毎に 10 秒ずつ待たされるという結果になりました。

ただし、Fiddler を使うと話が違ってきます。Fiddler は IP アドレス 127.0.0.1 のプロキシなのですが、どこかで IP アドレスから localhost と判定されているようで、同時接続数は制限されないという結果になりました。下の画像がその結果です。

Fiddler 経由で surferonwww.info を要求

ServicePoint.ConnectionLimit を取得するとデフォルトの同時接続数 2 となっていますが、実際の動きを見るとそれは無視されていて (Fiddler の画面を見ていると一度に 5 つ要求が出るのが分かります)、応答が返ってきた時間(end の時間)が 5 つとも同じになっています。

開発時に Fiddler で要求・応答をキャプチャして見るということはよく行うと思いますが、Fiddler の有無で同時接続数が異なるというのも要注意と思いました。

もう一つ、開発マシンのローカル IIS で動く ASP.NET Web Forms アプリに、hosts ファイルで 127.0.0.1 に websiteproject.com というホスト名を付けて、そのホスト名で呼び出せるようにし、それを下のサンプルコードで同時に 5 回要求した場合はどうなるを試してみました。結果は以下の画像の通りです。

websiteproject.com の場合

画像の end を見るとデフォルトの同時接続数 2 は有効になっているようで、2 要求毎に 10 秒ずつ待たされるという結果になっています。要求先の IP アドレスは 127.0.0.1 なのですが、.NET Framework ライブラリは IP アドレスでではなくて localhost という名前で判定しているのでしょうか。

ただし、ServicePoint.ConnectionLimit が Int32.MaxValue になっているのが解せません。

さらに、Fiddler を使うとどうなるかと言うと、上のケースと同様に同時接続数は制限されないという結果になりました。下の画像がその結果です。

Fiddler 経由で websiteproject.com を要求

この時は、ServicePoint.ConnectionLimit が何故か 2 になるのですが、これも解せません。

ソースコードを Microsoft のサイト ServicePoint.csServicePointManager.cs で見ることができるのですが、localhost か否かの判定はどのタイミングでどのようにしているのか、何がどのタイミングで ConnectionLimit プロパティの getter を呼び出して同時接続数を Int32.MaxValue に設定しているのか等々は読み切れませんでした。

ServicePointManager.DefaultConnectionLimit や ServicePoint.ConnectionLimit の値など、いろいろ不可解な動きに見えるのですが、ServicePoint.cs のソースコードのコメントに、

3. If ServicePoint.DefaultConnectionLimit is set, then take that value

4. If ServicePoint is localhost, then set to infinite (TO Should we change this value?)

・・・と書いてある通りで、とにかく localhost はデフォルトでは同時接続数は Int32.MaxValue になるということは間違いなさそうです。

最後に、検証に使った HttpWebRequest のコンソールアプリのコードを載せておきます。Visual Studio 2019 のテンプレートで .NET Framework 4.8 で作ったものです。

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleAppAsync4
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // localhost
            // ローカルの IIS Express で動くASP.NET MVC5 アプリの
            // アクションメソッド。
            // 要求を受けて 10 秒後に OK という文字列を返す
            var uri = "https://localhost:44365/Home/sample";

            // surferonwww.info
            // レンタルサーバーの IIS で動く ASP.NET Web Forms ア
            // プリの HTTP ジェネリックハンドラ。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            //var uri = "http://surferonwww.info/Test/Sample.ashx";

            // 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";

            Console.WriteLine($"ServicePointManager.DefaultConnectionLimit = " +
                              $"{ServicePointManager.DefaultConnectionLimit}");
            
            var sp = ServicePointManager.FindServicePoint(new Uri(uri));
            Console.WriteLine($"ServicePoint.ConnectionLimit = " +
                              $"{sp.ConnectionLimit}");

            var encoding = new UTF8Encoding(false);
            var tasks = new List<Task>();

            // 同じ URL を 5 回同時に要求する。実際に Web サーバーに
            // いくつ同時に要求が出るかは ConnectionLimit による (はず)
            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}, ";
                    
                    var request = (HttpWebRequest)WebRequest.Create(uri);
                    using (var response = await request.GetResponseAsync())
                    using (var stream = response.GetResponseStream())
                    using (var memory = new System.IO.MemoryStream())
                    {
                        await stream.CopyToAsync(memory);
                        var text = encoding.GetString(memory.ToArray());

                        // 終了時刻と ServicePoint.ConnectionLimit
                        string end = $"end: {DateTime.Now:ss.fff}";
                        string limit = $", limit: {sp.ConnectionLimit}";
                        text += start + end + limit;

                        Console.WriteLine(text);
                    }
                });

                tasks.Add(task);
            }

            await Task.WhenAll(tasks);

            Console.WriteLine("Finish");
            Console.ReadLine();
        }
    }
}

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  July 2021  >>
MoTuWeThFrSaSu
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar