WebSurfer's Home

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

非同期 HTTP ハンドラ (2)

by WebSurfer 2018年3月8日 18:56

先の記事「非同期 HTTP ハンドラ」で、Web サービスに非同期でアクセスする HTTP ジェネリックハンドラのサンプルを書きました。

その HTTP ハンドラは IHttpAsyncHandler インターフェイスを継承して BeginProcessRequest, EndProcessRequest メソッドを実装するという旧来の方法を使っており、記事に書いてありますように非常に複雑なコードになります。

ここまで複雑なことをしなければならないのなら、HTTP 503 エラー (Server Too Busy) が頻発して困っているというような事情がなければ、従来通り同期版のままでもいいかなって気もします。(笑)

ですが、.NET 4.5 から簡単に非同期呼び出しが実装できるようになったそうです。どのぐらい簡単にできるのか、同じ機能を async / await を利用して実装してみました。

非同期 HTTP ハンドラ

確かにはるかに簡単でした。前のように、何がどうなっているのかを調べて、悩んで、時間をかけて実装するという手間は大幅に減っています。

上の画像は、この記事に書いた方法で作成した HTTP ハンドラを、ブラウザから直接呼び出した結果です。具体的にどのように実装したかは以下に書きます。

HelloWorldWcfService.cs

先の記事の Web サービスは WCF サービスに変更しました。HelloWorld メソッドの実装は先の記事の Web サービスのものと全く同じです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class HelloWorldWcfService : IHelloWorldWcfService
{
    public string HelloWorld(int callDuration)
    {
        Thread.Sleep(callDuration);
        return String.Format(
          "Hello World from WcfService. Call Time: {0}",
          callDuration);
    }
}

WCF サービスに変更したのは、Web サービスが Legacy Technology だからということもありますが、一番の理由はサービス参照の追加を行うと自動生成されるサービスプロキシに含まれる非同期版メソッドが利用できるからです。

(非同期版メソッドについて、詳しくは先に記事「WCF サービスの非同期呼び出し」を見てください)

HelloWorldAsyncHnadler.ashx

Visual Studio で、既存の ASP.NET Web Forms アプリにジェネリックハンドラーを追加します。.ashx ファイルには何も手を加える必要はありません。

<%@ WebHandler Language="C#"
    CodeBehind="HelloWorldAsyncHandler.ashx.cs"
    Class="WebFormsApp.HelloWorldAsyncHandler" %>

Web アプリケーションプロジェクトの場合、.ashx ファイルとそのコードビハインドの .ashx.cs ファイルは別になります。Web サイトプロジェクトの場合、すべて .ashx ファイルに含まれるという違いがあります。

HelloWorldAsyncHandler.ahsx.cs

自動生成されたコードは同期版ハンドラのベースとなるものです。これを HttpTaskAsyncHandler クラスを継承したクラスに書き換えます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using WebFormsApp.HelloWorldServiceReference;

namespace WebFormsApp
{
    public class HelloWorldAsyncHandler : HttpTaskAsyncHandler
    {
        HelloWorldWcfServiceClient client;

        // これが呼び出されることはない。万一呼び出されたら例外
        // をスローして自爆する
        public override void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException();
        }
        
        public override async Task ProcessRequestAsync(
                                            HttpContext context)
        {
            context.Response.Write(
                "<p>Before await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            client = new HelloWorldWcfServiceClient();

            string result = await client.HelloWorldAsync(3000);

            context.Response.Write(
                "<p>After await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            context.Response.Write("<p>" + result + "</p>");
        }

        public override bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上のコードの WebFormsApp.HelloWorldServiceReference はサービスプロキシの名前空間です。HelloWorldWcfServiceClient コンストラクタ、HelloWorldAsync メソッドの定義は自動生成される Reference.cs ファイルにあります。

結局は先の記事「非同期 HTTP ハンドラ」と同様なことを行っていて、先の記事のコードの BeginProcessRequest メソッドが await キーワードの前に、EndProcessRequest メソッドが await の後に実行されているようです。

BeginProcessRequest, EndProcessRequest メソッドをプログラマがコードを書いて実装しなくて済むよう、コンパイラが肩代わりしているという感じです。

Tags: , ,

ASP.NET

HTTP ハンドラでキャッシュコントロール

by WebSurfer 2015年5月19日 17:55

このブログでは、アプリケーションのフォルダに保存した画像やスクリプトファイルなどを応答として返すのに HTTP ハンドラを使っています。

F5 キーを押すなどしてページをリロードすると、下の画像のように HTTP 304 Not Modified 応答を返しますが、これを HTTP ハンドラで実現するにはどのようにするかという話をメインに、HTTP ハンドラを利用したキャッシュコントロールについて書きます。

HTTP 304 応答

HTTP ハンドラを使わないで、以下のように直接 img 要素の src 属性に画像ファイルの URL を設定した場合は、IIS が ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めて返してくれます。

<img alt="" src="/images/sample2.jpg" />

クライアントが F5 キーなどでページをリロードすると、再度ブラウザが src 属性に設定された URL の画像を要求しますが、そのとき If-Modified-Since と If-None-Match を要求ヘッダに含めて送ってくれます。

要求を受けた IIS は要求ヘッダに含まれる If-Modified-Since と If-None-Match を見て、条件を満たせば(即ち、画像が変更されてなければ) HTTP 304 Not Modified 応答を返します(ヘッダのみでコンテンツは送信しない。ブラウザのキャッシュを使えという指示と同じ)。

ちなみに、ブラウザのキャッシュを削除すると、画像を要求する際 If-Modified-Since と If-None-Match は要求ヘッダには含まれなくなます。なので、キャッシュを削除してから F5 キーを押せば、IIS は画像のコンテンツを含めて HTTP 200 応答を返します。

src 属性に HTTP ハンドラを指定する(HTTP ハンドラ経由でファイルをダウンロードする)場合は、IIS が自動的に行ってくれる上記の処置を、自力で HTTP ハンドラにコーディングすることになります。

簡単に書くと、(1) ブラウザ送られてくる要求ヘッダの中の If-None-Match, If-Modified-Since の値を取得し、その値に応じて HTTP 200 応答(コンテンツを含む)または HTTP 304 応答(コンテンツ無し)のどちらかを返す。(2) 要求を受けたら毎回、応答ヘッダの ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めてブラウザへ返す・・・という処置をコーディングすることになります。

さらに、せっかく HTTP ハンドラを使って細かくキャッシュコントロールのためのコードが書けるのですから、上記 (1), (2) だけでなく(ETag, Last Modified の応答ヘッダへの設定だけでなく)、Cache-Control, Expires 等の応答ヘッダへの設定も行って、プロキシやブラウザのキャッシュをきちんとコントロールした方がよさそうです。

また、サーバー側でのキャッシュもコントロールすることが可能になりますので、サーバーのキャッシュが利用できればそれを使わない手はないですよね。

ということで、例として、画像ファイル用とスクリプトファイル用それぞれの HTTP ハンドラを、Web サイトプロジェクトの ジェネリックハンドラ(.ashx ファイル)の形で書いたコードを以下に紹介します。ベースは BlogEngine 1.6.1.0 のものです。

(1) 画像用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ImageHandler" %>

using System;
using System.Web;
using System.IO;
using System.Net;

public class _0114_ImageHandler : IHttpHandler {
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;
        
    // ファイル名はクエリ文字列で指定。
    string fileName = request.QueryString["picture"];
        
    // 画像ファイルはアプリケーションルート直下の
    // images という名前のフォルダにあることが前提。
    string folder = "~/images/";

    if (!String.IsNullOrEmpty(fileName))
    {            
      string path = 
          context.Server.MapPath(folder + fileName);
      FileInfo fileInfo = new FileInfo(path);
      if (fileInfo.Exists)
      {            
        // BlogEngine では何故か作成日時 (CreationTimeUtc) が
        // 使われていたが、IIS が設定するのと同様に、更新日時
        // (LastWriteTimeUtc) を使用するように変更。
        DateTime lastWriteTime = fileInfo.LastWriteTimeUtc;

        // ETag は更新日時のタイマ刻み数。(IIS とは異なる)
        string etag = "\"" + lastWriteTime.Ticks + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];

        DateTime ifModifiedSince = DateTime.MinValue;
        DateTime.TryParse(
            request.Headers["If-Modified-Since"], 
            out ifModifiedSince);

        response.Clear();
        // ブラウザとプロキシにキャッシュを許可
        response.Cache.SetCacheability(HttpCacheability.Public);
        // 有効期限を、要求を受けた日時から 1 年に設定。
        response.Cache.SetExpires(DateTime.Now.AddYears(1));
        // Last-Modified を応答ヘッダに設定。
        response.Cache.SetLastModified(lastWriteTime);
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);

        // 要求ヘッダの If-None-Match, If-Modified-Since と、
        // 上のコードで取得した etag, lastWriteTime を比較し、
        // どちらかが一致したら HTTP 304 応答を返して終了。
        if (ifNoneMatch == etag || 
            ifModifiedSince == lastWriteTime)
        {
          response.StatusCode = 
              (int)HttpStatusCode.NotModified;
          return;
        }

        int index = fileName.LastIndexOf(".") + 1;
        string extension = 
            fileName.Substring(index).ToLowerInvariant();

        // IE は image/jpg を認識しないことへの対応
        if (extension == "jpg")
        {
          context.Response.ContentType = "image/jpeg";
        }
        else
        {
          context.Response.ContentType = "image/" + extension;
        }
                
        response.TransmitFile(fileInfo.FullName);
      }
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ETag, Last Modified, Expires の違いについては以下の stackoverflow の記事が参考になると思います。

(2) スクリプト用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ScriptHandler" %>

using System;
using System.Web;
using System.IO;
using System.Security;
using System.Web.Caching;
using System.Net;

public class _0114_ScriptHandler : IHttpHandler 
{
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;

    // ファイル名はクエリ文字列で指定。
    string fileName = request.QueryString["filename"];

    // スクリプトファイルはアプリケーションルート直下の
    // scripts と言う名前のフォルダにあることが前提。
    string folder = "~/scripts/";

    if (!String.IsNullOrEmpty(fileName))
    {
      string script = null;
            
      // まずサーバーのCacheをチェック。なければファイルに
      // アクセスしてスクリプトを取得。
      if (context.Cache[fileName] == null)
      {
        if (!fileName.EndsWith(".js",
            StringComparison.OrdinalIgnoreCase))
        {
          throw new SecurityException("アクセス不可");
        }

        string path =
            context.Server.MapPath(folder + fileName);
        FileInfo fileInfo = new FileInfo(path);

        if (fileInfo.Exists)
        {
          using (StreamReader reader = 
                              new StreamReader(path))
          {
            script = reader.ReadToEnd();
                        
            // Minify は割愛。
                        
            // サーバーのキャッシュに保持。
            context.Cache.Insert(
                        fileName,
                        script,
                        new CacheDependency(path));
          }
        }
      }
      else
      {
        script = (string)context.Cache[fileName];
      }

      if (!string.IsNullOrEmpty(script))
      {
        response.Clear();
        response.ContentType = "text/javascript";

        // Vary: Accept-Encoding をヘッダに設定。
        response.Cache.VaryByHeaders["Accept-Encoding"] = true;

        // 有効期限 (Expires ヘッダ) を 7 日に設定。
        response.Cache.SetExpires(
                DateTime.Now.ToUniversalTime().AddDays(7));
        // Cache-Control ヘッダの max-age を 7 日(604800 秒)
        // に設定。
        response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
        // Cache-Control ヘッダに must-revalidate を設定。
        response.Cache.SetRevalidation(
                HttpCacheRevalidation.AllCaches);

        int hash = script.GetHashCode();
                
        string etag = "\"" + hash.ToString() + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];
                
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);
        // Cache-Control ヘッダに public を設定(ブラウザと
        // プロキシにキャッシュを許可)
        response.Cache.SetCacheability(HttpCacheability.Public);
                
        // 要求ヘッダの If-None-Match と上のコードで取得した
        // etag を比較し一致したら HTTP 304 応答を返して終了。
        // サーバーの Cache を利用する関係で Last Modified
        // は使用しない。
        if (ifNoneMatch == etag)
        {
          response.StatusCode = 
                      (int)HttpStatusCode.NotModified;
          return;
        }

        response.Write(script);
      }            
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ファイルから取得したスクリプトの文字列をサーバーのキャッシュに保持するようにし、2 回目以降の要求ではキャッシュから取得するようにしています。

キャッシュから取得する場合はスクリプトファイルの更新日時を取得できないので、Last Modified は使わず ETag のみを応答ヘッダに含めています。

ETag の値は、スクリプトの文字列から String.GetHashCode メソッドでハッシュコードを取得し、それを設定しています。

Tags:

Cache

ジェネリックハンドラー

by WebSurfer 2014年12月16日 14:42

Web サイトプロジェクトでジェネリックハンドラー(.ashx)を作成する際、コードビハインド形式では作成できないのですが、それは何故かについて書きます。(どうでもいい話かもしれませんが)

ジェネリックハンドラーの作成

上の画像は Visual Studio 2010 のソリューションエクスプローラーから「新しい項目の追加」ダイアログを開いてジェネリックハンドラーを追加しようとしているところです。[別のファイルにコードを書き込む(P)]のオプションがグレーアウトされて選択できないのがわかるでしょうか? そのまま作業を進めると、.ashx ファイルのみが生成されます。

一方、Web アプリケーションプロジェクトの場合は[別のファイルにコードを書き込む(P)]というようなオプションはそもそも表示されません(ジェネリックハンドラに限らず Web フォームなど他の項目も同様ですが)。自動的にコードビハインド形式になります(.ashx ファイルと .ashx.cs ファイルが作成されます)。

(注:Web アプリケーションプロジェクトと Web サイトプロジェクト の違いについては MSDN ライブラリの記事をリンクしたのでそこを見てください)

Web サイトプロジェクトでコードビハインド形式で作成できない理由が書いてある Microsoft の文書は見つけられなかったのですが、MSDN ライブラリ HTTP ハンドラの概要 のページの「ファイル名拡張子の作成」のセクションの以下の記述が関係ありそうです。

"ファイル名拡張子 .ashx に対応する HTTP ハンドラクラスを作成した場合、ハンドラは IIS と ASP.NET に自動的に登録されます。"

"ハンドラに対応するカスタムファイル名拡張子を作成する場合は、この拡張子を IIS と ASP.NET に明示的に登録する必要があります。"

引用文の前者がこの記事で言うジェネリックハンドラーのことで、HTTP ハンドラ本体(IHttpHandler を継承するクラス)とハンドラマッピング(@ WebHandler ディレクティブと Class 属性定義)を同一ファイル内に実装して .ashx という拡張子を付与し、IIS と ASP.NET に自動的に登録されるようにしたものです。

引用文の後者は、一般的な HTTP ハンドラのことで、HTTP ハンドラ本体を普通のクラスファイルに実装し、ソースコードのまま App_Code フォルダに置くか、コンパイルして dll を作って Bin フォルダに配置し、web.config にハンドラマッピングを定義するということになります。

つまり、ジェネリックハンドラーは、いちいち web.config にハンドラマッピングの定義を書かなくても、例えば <img src=handler.ashx ... /> のようにするだけで簡単に使えます。

コードビハインド形式にする(.ashx ファイルと .ashx.cs ファイルに分ける)のは取り扱いが面倒になるだなので、Web サイトプロジェクトでジェネリックハンドラーをコードビハインド形式で作成するオプションがないのだと思います。

Web アプリケーションプロジェクトではコード部分を分けざるを得ませので、選択の余地なく .ashx ファイルと .ashx.cs ファイルが分けて生成されます。ただし、Visual Studio からは .ashx.cs ファイルしか開けません(ファイルは存在しますが Visual Studio 上では開けません)。


以下は余談ですが、「コードビハインド」という呼び方について一言・・・

本来、HTML デザインブロックの背後にある ASP.NET 処理ブロック(.aspx.cs, .master.cs, .ascx.cs などに定義されるクラス)をコードビハンドと呼ぶようです。なので、.ashx ファイルの場合はコードビハンドという呼び方ではなく「HTTP ハンドラ本体」と言う方が適切かもしれません。

また、@ Page ディレクティブの CodeBehind 属性の説明を読むと "この属性は、以前のバージョンの ASP.NET との互換性を保持して、分離コード機能を実装するために用意されています。ASP.NET Version 2.0 では、代わりに CodeFile 属性を使用してソース ファイルの名前を指定すると同時に、Inherits 属性を使用してクラスの完全修飾名を指定します" とのことです。

@ WebHandler ディレクティブにも CodeBehind 属性はありますが、MSDN ライブラリによると "この属性は実行時には使用されません。この属性は、以前のバージョンの ASP.NET と互換性を保つために含まれています" とのことです。

なので「コードビハインド」という呼び方自体が古いのかもしれません。

Tags:

ASP.NET

About this blog

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

Calendar

<<  2018年6月  >>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar