WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

localhost への同時接続数

by WebSurfer 2021年6月18日 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

HttpWebRequest で WCF サービスを呼出

by WebSurfer 2017年3月26日 14:36

先の記事 WCF と jQuery AJAX では、JSON 文字列をデータとしてやり取りする WCF サービスのメソッドを、jQuery.ajax を使って呼び出してデータを取得する方法を書きました。

この WCF サービスのメソッドを HttpWebRequest / HttpWebResponse を利用して呼び出して JSON 文字列のデータを取得し、それを逆シリアル化して C# のオブジェクトに変換する方法を書きます。

ここでは例として先の記事の WCF サービスの GetCarsByDoors(int doors) メソッドを POST 要求してみます。

まず、JSON 文字列が逆シリアル化された際の C# のクラス / プロパティを書き、それらに DataContract / DataMember 属性を付与してデータコントラクトを定義します。

JSON 文字列から C# のクラス / プロパティの変換は json2csharp のような変換サービスを利用すると簡単にできると思います。

先の記事のコードでは、WCF サービスの GetCarsByDoors(int doors) メソッドの応答の JSON 文字列は "GetCarsByDoorsResult" でラップされるように設定されていますので、それを考慮して以下のようなデータコントラクト定義になります。

[DataContract]
public class RootObject
{
    // GetCarsByDoorsResult は WCF サービスメソッドに付与した
    // BodyStyle = WebMessageBodyStyle.WrappedRequest による
    // ラップの名前(ラップするのはセキュリティ対策)
    [DataMember]
    public List<Car> GetCarsByDoorsResult { get; set; }
}

[DataContract]
public class Car
{
    [DataMember]
    public string Make { get; set; }

    [DataMember]
    public string Model { get; set; }

    [DataMember]
    public int Year { get; set; }

    [DataMember]
    public int Doors { get; set; }

    [DataMember]
    public string Colour { get; set; }

    [DataMember]
    public float Price { get; set; }
}

HttpWebRequest を利用して WCF サービスの GetCarsByDoors(int doors) メソッドを要求し JSON 文字列をデータとして POST 送信します。ここでは例として "{\"doors\":5}" という 5 ドア車を要求する JSON 文字列を設定しています。

HttpWebResponse を利用して応答を取得し、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズします。

詳しくは以下のサンプルコードとそれに付与したコメントを見てください。

using System;
using System.Net;
using System.Text;
using System.IO;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;

namespace ConsoleApplication7
{
  class Program
  {
    static void Main(string[] args)
    {
      // 指定した Uri を要求する HttpWebRequest を初期化
      HttpWebRequest endpointRequest =
          (HttpWebRequest)HttpWebRequest.
          Create("http://.../carservice.svc/GetCarsByDoors");

      endpointRequest.Method = "POST";
      endpointRequest.ContentType = 
          "application/json; charset=utf-8";

      // POST データの設定。とりあえず 5 ドアを要求してみる
      string postData = "{\"doors\":5}";

      Encoding encoding = Encoding.GetEncoding("utf-8");
      byte[] byte1 = encoding.GetBytes(postData);
      endpointRequest.ContentLength = byte1.Length;

      // POST データを書き込むストリームを取得
      using (Stream requestStream = 
             endpointRequest.GetRequestStream())
      {
        // POST データを要求ストリームに書き込み
        requestStream.Write(byte1, 0, byte1.Length);

        // WCF サービスメソッドからの応答を取得
        using (HttpWebResponse endpointResponse =
               (HttpWebResponse)endpointRequest.GetResponse())
        {
          // 応答のコンテンツを読むストリームを取得
          using (Stream responseStream = 
                 endpointResponse.GetResponseStream())
          {
            // JSON シリアライザの初期化
            DataContractJsonSerializer ser = 
              new DataContractJsonSerializer(typeof(RootObject));

            // 応答のコンテンツを逆シリアル化して C# の
            // オブジェクトを取得
            RootObject rootObject = 
              (RootObject)ser.ReadObject(responseStream);
                        
            foreach (Car car in rootObject.GetCarsByDoorsResult)
            {
              Console.WriteLine("Make:{0}, Model:{1}, Doors:{2}",
                                car.Make, car.Model, car.Doors);

            /*
            結果は:
            Make:Audi, Model:A4, Doors:5
            Make:Ford, Model:Focus, Doors:5
            Make:Renault, Model:Laguna, Doors:5
            Make:Toyota, Model:Previa, Doors:5
            */

            }
          }
        }
      }
    }
  }
}

DataContractJsonSerializer クラスを利用したシリアル化 / 逆シリアル化については、MSDN ライブラリの記事「方法 : JSON データをシリアル化および逆シリアル化する」が参考になると思います。

Tags: ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar