WebSurfer's Home

Filter by APML

HttpClient の同時接続数

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 接続の数は無制限です"

・・・と書いてあって、わざわざコードを書いたりして調べるまでもなかったです。でも、せっかく調べたので、調べたことを以下に書いておきます。

surferonwww.info の場合

上の画像は、自分が使っているホスティングサービスの 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();
        }
    }
}

Tags: ,

CORE

Blazor Web App の WebAssembly モード

by WebSurfer 17. November 2024 18:15

Visual Studio 2022 を使って下の画像のように Blazor Web App をテンプレートに選んで作成した Blazor アプリは、予想に反して WebAssembly モードでも Server-Side Rendering (SSR) になることがありました。以下にそのことを書きます。

Blazor Web App を選択

作成時に [Interactive render mode] を [WebAssembly] に設定しても、[Interactive location] に [Per page/component] を選んだ場合(多くの人はこちらを選ぶのではなかろうかと思います)、

[WebAssembly], [Per page/component] に設定

アプリを起動して [Home], [Counter], [Weather] ボタンをクリックして画面を切り替えると、

アプリを起動

下の Fiddler によるキャプチャ画像の通り、

Fiddler によるキャプチャ画像

毎回サーバーに要求を出し、完全な html ソースをサーバーから応答として受け取り、それをブラウザに表示するという SSR になります。WebAssembly だからサーバーとのやり取りはしない、即ち CSR になることを期待していましたがそうはなりません。

Microsoft のドキュメント ASP.NET Core Blazor render modes(日本語版もありますが翻訳がアレなので英語版がお勧め)の Render modes のセクションに説明があります。

そこにはコンポーネントで @rendermode を InteractiveWebAssembly に設定すると Client-side rendering (CSR) using Blazor WebAssembly となって Render location が Client になると書いてあります。

なので、試しに Home, Counter, Weather 全てのコンポーネントで @rendermode InteractiveWebAssembly を設定してみました。しかし、相変わらずサーバとのやり取りを行う SSR になります。

プロジェクトを作成する際に [Interactive location] を [Global] に設定すればサーバーとのやり取りはなくなり CSR になります。コンポーネントに @rendermode InteractiveWebAssembly を設定する必要はないようで、自動生成される各コンポーネントには @rendermode の設定は含まれていません。

知ってました? 自分は知らなかったです。 [Interactive render mode] を [WebAssembly] に設定するだけで無条件で CSR になると思ってました。WebAssembly のメリットは CSR のはずなので、勝手に知らないうちに SSR になっては困ると思うのですが・・・

ちなみに、テンプレートに Blazor WebAssembly を使って作成した Blazor アプリの場合は [Home], [Counter], [Weather] ボタンをクリックして画面を切り替えてもサーバーとの通信は発生せず CSR になります。


そのあたりのことを書いた Microsoft のドキュメントは見つからなかったので Copilot に聞いてみました。回答を載せておきます。(ホントかどうかの裏は取ってませんのでご参考まで)

(1) Blazor Web App と Blazor WebAssembly の違いに関して

The difference you're seeing between the two cases is indeed a bit subtle but has to do with how the Blazor Web App and Blazor WebAssembly templates are configured and what each is optimized for.

Blazor Web App

Interactive Render Mode: When you set the interactive render mode to WebAssembly but also have it set to Per page/component, Blazor is likely configured to request the server to render the pages/components interactively. This is why you're seeing GET requests being made to the server.

Behavior: Even though WebAssembly is being used, each time you navigate, a new GET request is made to the server. This means the server is still involved in delivering the HTML content, and the WebAssembly part is handling the interactive functionalities of the individual components on the page. This can be useful if you want the server to control the navigation and then WebAssembly to handle the client-side interactivity.

Blazor WebAssembly

Pure Client-Side Rendering: This template is optimized for a purely client-side experience. Once the initial application is loaded, all subsequent navigation and component rendering are handled entirely by the client without additional requests to the server.

Behavior: This makes it behave like a traditional Single Page Application (SPA) where the entire application, including navigation between pages, is handled within the client browser using WebAssembly. This is why you're not seeing additional requests to the server when navigating between Home, Counter, and Weather.

To summarize, the main difference is in how much the server is involved in rendering pages. In the Blazor Web App with interactive mode set to WebAssembly and per page/component, the server still plays a role in serving up pages, whereas, in Blazor WebAssembly, once the application is loaded, the server is not involved in further page rendering. This setup can affect performance and user experience depending on your application's needs.


(2) [Interactive location] を [Global] に設定すると何が変わるか

You're right, changing the Interactive location to Global in a Blazor Web App with the WebAssembly interactive render mode would make it behave similarly to the pure Blazor WebAssembly app, where no additional server requests are made during navigation. This setup alters the application's structure in a way that makes the WebAssembly component responsible for rendering more globally, not just per page/component.

Configuration Differences

Project Structure:

Global Interactive Location: This mode places components like Home.razor and Weather.razor under the .Client project, indicating that the client-side WebAssembly is handling the rendering globally for all these components.

Per Page/Component Interactive Location: In this mode, each page or component might still be interacting with the server for rendering purposes, leading to the behavior you observed with additional server requests.

Component Loading:

Global Interactive Location: The client-side WebAssembly loads the components globally, so once the application is loaded, navigation between different pages like Home, Counter, and Weather is handled entirely on the client side.

Per Page/Component Interactive Location: Each navigation may involve a server request because the rendering is done on a per-component basis, making the server more involved in the process.

Potential Reasons for Similar Program.cs

Shared Configuration: The Program.cs file might not differ significantly because the core configuration and service registrations for Blazor WebAssembly applications are quite similar. The main difference lies in how the application components are managed and rendered within the project structure and specific component files.

Conclusion

By setting the interactive location to Global, you've essentially optimized the application to function more like a typical SPA where the client-side WebAssembly handles most of the rendering and navigation logic, reducing the need for server interaction during page changes. This change primarily affects how components are structured and rendered rather than altering the fundamental configuration in Program.cs.

Tags: , , , ,

CORE

Blazor WASM から ASP.NET Core Web API を呼び出し

by WebSurfer 29. June 2024 11:42

ASP.NET Core Blazor Web Assembly (WASM) からトークン (JWT) ベースの認証が必要な ASP.NET Core Web API にクロスドメインでアクセスしてデータを取得するサンプルを作ってみました。以下に作り方を備忘録として書いておきます。

結果の表示

Visual Studio 2022 のテンプレートを利用して ASP.NET Core Web API と Blazor WASM のソリューションを別々に作成します。完成後、Visual Studio 2022 から両方のプロジェクトを実行し ([デバッグ(D)]⇒[デバッグなしで開始(H)])、Blazor WASM から Web API に要求を出して応答を Blazor WASM の画面上に表示したのが上の画像です。

以下に、まず Web API アプリの作り方、次に Blazor WASM アプリの作り方を書きます。

(1) Web API アプリ

(1.1) プロジェクトの作成

元になる ASP.NET Core Web API アプリのプロジェクトは Visual Studio 2022 V17.10.3 のテンプレートで自動生成されたものを使いました。プロジェクトを作成する際「追加情報」のダイアログで「認証の種類(A)」は「なし」にします。この記事ではターゲットフレームワークは .NET 8.0 にしました。

自動生成されたプロジェクトにはサンプルのコントローラ WeatherForecastController が実装されていて、Visual Studio からプロジェクトを実行し、ブラウザから WeatherForecast を要求すると JSON 文字列が返ってきます。

これに JWT ベースの認証を実装し(即ち、トークンが無いとアクセス拒否するようにし)、さらに Blazor WASM からクロスドメインで呼び出せるようにするため CORS を実装します。

加えて、クライアントからの要求に応じてトークンを発行するための API も追加で実装します。この記事では、トークン要求の際クライアントから ID とパスワードを送信してもらい、それらが既存の ASP.NET Core Identity で有効であることを確認してからトークンを返すようにします。無効の場合は HTTP 401 Unauthorized 応答を返します。

(1.2) NuGet パッケージのインストール

下の画像の赤枠で囲んだ Microsoft.AspNetCore.Authentication.JwtBearer を NuGet からインストールします。

NuGet パッケージのインストール

青枠で囲んだものは、上に述べたトークン発行の際の既存の ASP.NET Core Identity によるユーザー認証を行うために必要です。ASP.NET Core Identity を使わない場合(例えば、ユーザー認証なしで無条件にトークンを返すようにする場合)は必要ありません。

(1.3) JWT 認証スキーマを登録

自動生成された Program.cs に、AddAuthentication メソッドを使って JWT 認証スキーマを登録するコードを追加します。加えて、認証を有効にするため app.UseAuthentication(); も追加します。

app.UseAuthentication(); は既存のコードの app.UseAuthorization(); の前にする必要があるので注意してください。

具体的には以下のコードで「JWT ベースの認証を行うため追加」とコメントしたコードを追加します。

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebApi.Data;
using Microsoft.EntityFrameworkCore;

namespace WebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // JWT ベースの認証を行うため追加
            builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = builder.Configuration["Jwt:Issuer"],
                        ValidAudience = builder.Configuration["Jwt:Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
                    };
                });

            // ・・・中略・・・

            // JWT ベースの認証を行うため追加
            app.UseAuthentication();

            //・・・後略・・・

(1.4) Key と Issuer を appsettings.json に登録

上の (1.3) コードでは Key と Issuer を appsettings.json ファイルより取得するようにしていますので、以下のように "Jwt" 要素を追加します。

{

  ・・・中略・・・

  "AllowedHosts": "*",
  "Jwt": {
    "Key": "veryVerySecretKeyWhichMustBeLongerThan32",
    "Issuer": "https://localhost:44366"
  }
}

Key はパスワードのようなもので任意の文字列を設定できます。32 文字以上にしないとエラーになるので注意してください。.NET Core 3.1 時代は 16 文字以上で良かったのですが、いつからか 32 文字以上に変わったらしいです。Issuer はサービスを行う URL にします。

(1.5) [Authorize] 属性を付与

自動生成された WeatherForecastController コントローラの Get() メソッドに [Authorize] 属性を付与します。

ここまでの設定で JWT トークンベースのアクセス制限の実装は完了しており、トークンなしで WeatherForecastController コントローラの Get() メソッドを要求すると HTTP 401 Unauthorized 応答が返ってくるはずです。

(1.6) トークンを発行する API を実装

クライアントから送信されてきた ID とパスワードでユーザー認証を行った上でトークンを発行する API を実装します。以下のコードでは、UserManager<IdentityUser> オブジェクトへの参照を DI によって取得し、それを使って既存の ASP.NET Core Identity から情報を取得してユーザー認証に用いています。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TokenController : ControllerBase
    {
        private readonly IConfiguration _config;
        private readonly UserManager<IdentityUser> _userManager;

        public TokenController(IConfiguration config,
                               UserManager<IdentityUser> userManager)
        {
            _config = config;
            _userManager = userManager;
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> CreateToken(LoginModel login)
        {
            string? id = login.Username;
            string? pw = login.Password;
            IActionResult response = Unauthorized();

            if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(pw))
            {
                var user = await _userManager.FindByNameAsync(id);
                if (user != null && 
                    await _userManager.CheckPasswordAsync(user, pw))
                {
                    var tokenString = BuildToken();
                    response = Ok(new { token = tokenString });
                }
            }

            return response;
        }


        private string BuildToken()
        {
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));

            var creds = new SigningCredentials(
                key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(
                issuer: _config["Jwt:Issuer"],
                audience: _config["Jwt:Issuer"],
                claims: null,
                notBefore: null,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    public class LoginModel
    {
        public string? Username { get; set; }
        public string? Password { get; set; }
    }
}

既存の ASP.NET Core Identity から情報を取得してユーザー認証を行うためには上記以外にも以下の (a) ~ (d) の追加が必要です。

た���し、ユーザー認証など面倒なことはしないで、CreateToken メソッドが呼ばれたら無条件にトークンを発行して返せばよいという場合は不要です。上のコードの UserManager<IdentityUser> オブジェクトの DI を行う部分も不要です。

(a) 上の (1.2) の画像で青枠で囲んだ NuGet パッケージのインストール。

(b) IdentityDbContext を継承した ApplicationDbContext クラスを追加。Data フォルダを作ってそれにクラスファイルとして実装します。

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace WebApi.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(
            DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}

(c) appsettings.json に ASP.NET Core Identity が使う既存の SQL Server DB への接続文字列を追加。

(d) Program.cs に以下の「// 追加」とコメントしたコードを追加。これらは上の「(1.3) JWT 認証スキーマを登録」に書いたコード builder.Services.AddAuthentication(...); より前に追加する必要があるので注意してください。

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebApi.Data;
using Microsoft.EntityFrameworkCore;

namespace WebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // 追加
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    builder.Configuration.GetConnectionString(
                        "MvcCoreIdentityContextConnection")));

            // 追加
            builder.Services.AddDefaultIdentity<IdentityUser>()
                            .AddEntityFrameworkStores<ApplicationDbContext>();

            //・・・後略・・・

(1.7) CORS 機能の実装

Blazor WASM からクロスドメインで Web API を呼び出すため、Web API アプリに CORS 機能を実装します。

具体的には、Program.cs に以下のコードを追加します。プリフライトリクエストが行われますので AllowAnyHeader() と AllowAnyMethod() が必要です。AllowCredentials() はトークンベースの認証の場合は不要のようです。

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebApi.Data;
using Microsoft.EntityFrameworkCore;

namespace WebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // 追加
            var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
            builder.Services.AddCors(options =>
            {
                options.AddPolicy(name: MyAllowSpecificOrigins,
                                  policy =>
                                  {
                                      policy.AllowAnyOrigin()
                                            .AllowAnyHeader()
                                            .AllowAnyMethod();
                                  });
            });

            // ・・・中略・・・

            // 追加 
            app.UseCors(MyAllowSpecificOrigins);

            //・・・後略・・・

(2) Blazor WASM アプリ

(2.1) プロジェクトの作成

Visual Studio 2022 の新しいプロジェクトの作成で「Blazor WabAssembly アプリ」のテンプレート (Blazor Web App を選ばないよう注意)を使って自動生成されたものを使います。「追加情報」のダイアログで「認証の種類(A)」は「なし」にします。この記事ではターゲットフレームワークは .NET 8.0 にしました。

(2.2) Weather.razor の修正

自動生成されたプロジェクトの Weather.razor には、wwwroot 下の json ファイル weather.json を要求して、応答の JSON 文字列をデシリアライズして表示するコードが含まれています。

その部分のコードを、Web API からトークンを取得した後、トークンを要求ヘッダに含めて送信し、応答として返されたデータを表示するように変更します。

変更するのは @code ブロックのみで、以下の通りとなります。

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        // 以下はテンプレートで自動生成されたコードに含まれているもの
        // これを下のように書き換える
        // forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");

        var tokenUrl = "https://localhost:44366/api/token";
        var forecastUrl = "https://localhost:44366/WeatherForecast";

        // 送信する ID とパスワード。既存の ASP.NET Core Identity で有効なもの
        var credentials = new {
            Username = "oz@mail.example.com",
            Password = "myPassword"
        };

        // ID とパスワードを送信してトークンを取得
        // content-type: application/json; charset=utf-8 は自動的に
        // ヘッダに付与される
        using var tokenResponse = await Http.PostAsJsonAsync(tokenUrl, credentials);
        var jwt = await tokenResponse.Content.ReadFromJsonAsync<JWT>();

        if (jwt != null && !string.IsNullOrEmpty(jwt.Token))
        {
            // 取得したトークンを Authorization ヘッダに含めて GET 要求
            var request = new HttpRequestMessage(HttpMethod.Get, forecastUrl);
            request.Headers.Authorization = 
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwt.Token);
            using var forecastResponse = await Http.SendAsync(request);
            forecasts = await forecastResponse.Content.ReadFromJsonAsync<WeatherForecast[]>();
        }
    }

    // 追加
    public class JWT
    {
        public string? Token { get; set; }
    }

    public class WeatherForecast
    {
        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public string? Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

デスクトップアプリで使う HttpClient の場合とほぼ同じコードになりました。Blazor WASM のクライアント側はブラウザなので、HttpClient そのものが動くはずはなく、WebAssembly のコードに変換されてブラウザに送られたコードが動いているのではないかと思います。

この記事の例では Web API に対してはブラウザからのクロスドメインでの要求となり、かつシンプルなリクエストとはならないので、CORS 対応のためのプリフライトリクエストが必要なヘッダ情報を含めて送信されます。Fiddler で要求・応答をキャプチャするとそのあたりのことが分かります。

Fiddler で要求・応答をキャプチャ

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar