WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ダウンロードは HTTP ハンドラで

by WebSurfer 16. February 2013 16:50

画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。

.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。

MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。

このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。

そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。

従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。

以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。

  1. Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
  2. Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
  3. Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
  4. html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
  5. Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。

.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。

http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。

(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)

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

using System;
using System.Web;

public class Handler : IHttpHandler 
{
    public void ProcessRequest (HttpContext context) 
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;
        
        string fileName = "日本語.txt";        
        
        // IE の場合、日本語ファイル名の文字化け対策が必要。
        // Firefox, Chrome, Safari, Opera の場合は不用。

        // 2014/3/3 修正
        // IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
        // または "InternetExplorer" (.NET 4) になる。IE の場合
        // User Agent には必ず "Trident" と言う文字列が入ってい
        // るらしいので、そちらで判定した方がよさそう。
        if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
            || request.UserAgent.Contains("Trident"))
        {
            fileName = context.Server.UrlEncode(fileName);
        }
        
        // キャッシュを許可するか否か、許可する場合は有効期限を
        // 指定しておくべき。
        // 以下のコードはキャッシュを許可しない場合の例。応答ヘ
        // ッダーは次のようになる。
        //    Cache-Control: no-cache
        //    Pragma: no-cache
        //    Expires: -1
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
        response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

        // ブラウザによってファイルの種類を判断する方法が異なる。
        // IE は、Content-Disposition: ヘッダが存在する場合は、
        // filename パラメータで設定されたファイル名の拡張子を
        // 優先的に使う。Content-Type: ヘッダの指定は無視される。
        // 逆に、Opera など、Content-Type: ヘッダの指定を優先的
        // に使うものもある。なので、Content-Disposition: ヘッダ
        // と Content-Type: ヘッダの両方を正しく指定しておいた方
        // がよさそう。        
        response.AppendHeader("Content-Disposition",
            "attachment;filename=" + fileName);
        response.ContentType = "text/plain";

        // 文字列 "Hello World" を応答 HTTP ストリームに書込み。
        // ファイルの場合は TransmitFile メソッドを使うのがお勧
        // め(WriteFile メソッドは大きなファイルは扱えないので)
        response.Write("Hello World");
        
        // Flush, End, Close メソッド等は使用しないこと。
    }
 
    public bool IsReusable 
    {
        get 
        {
            return false;
        }
    }

}

呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。

Tags: ,

Upload Download

非同期 HTTP ハンドラ

by WebSurfer 23. September 2012 13:18
2018/3/8 追記:
この記事と同じ機能を持つ非同期 HTTP ハンドラを .NET 4.5 から利用できるようになった async / await を利用して実装してみました。その記事は「非同期 HTTP ハンドラ (2)」にありますので興味があれば見てください。この記事の方法が時代遅れなのが分かると思います。

ASP.NET の非��期プログラミングモデルには、(1) 非同期ページ、(2) 非同期 HTTP ハンドラ、(3) 非同期 HTTP モジュールの 3 つがあります。このうち、(2) 非同期 HTTP ハンドラを使って Web サービスのメソッドを非同期に呼び出す場合について、いろいろ不明な点があったので、それらを調べて分かったことを備忘録として書いておきます。

MSDN マガジンの March 2007 の記事「ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション (Wicked Code: Scalable Apps with Asynchronous Programming in ASP.NET)」が、非同期プログラミングの目的、メリット、仕組みなどをサンプルコードを使って詳しく説明しており、大変参考になりました。(2017/5/20 追記:記事は .chm ファイル形式で保存されており、MSDN Magazine Issues and Downloads からダウンロードして読むことができます)

(2015/12/29 追記:非同期プログラミングのメリット、仕組みなど基本的な説明を書いた新しい記事 ASP.NET の非同期/待機の概要を紹介しておきます。.NET 4.5 以降で利用できる async / await を使うことが前提です)

ただし、その中の非同期 HTTP ハンドラのサンプルは複雑すぎて、その仕組みなどがよく分かりませんでした。特に分からなかったのは以下の点です。

  1. Web サービスのメソッドを非同期で呼び出すメソッドをどのように生成するか(例えば、Web サービスのメソッドが HelloWorld だったとすると、BeginHelloWorld と EndHelloWorld をどのように作成するか)。
  2. 非同期 HTTP ハンドラのコードで非同期呼び出しのためのメソッド(上の例で言うと、BeginHelloWorld メソッドと EndHelloWorld メソッド)をどのように呼び出すか。
  3. BeginProcessRequest メソッドの第 2 引数として渡されるコールバックメソッドのデリゲートは何か。
  4. EndProcessRequest メソッドが呼び出される仕組みとそのタイミングはどうなっているか。

という訳で、簡単なサンプルを作って上記の点について調べてみました。下の画像は、説明のためにここに紹介したサンプルを実行したときのものです。

非同期 HTTP ハンドラの使用

サンプルとして、以下の Web サービスを非同期 HTTP ハンドラを使って利用することを考えます。これに定義されているのは HelloWorld メソッドだけです。同期 HTTP ハンドラではこれを呼べば済みますが、非同期 HTTP ハンドラを使う場合は非同期で HelloWorld メソッドを呼び出し、結果を処理しなければなりません。

非同期呼び出しのために、BeginHelloWorld メソッドと EndHelloWorld メソッドが必要ですが、それらはどのように作ればよいのでしょうか?

(1) Web サービス (141-HelloWorldWebService.asmx)

<%@ WebService Language="C#" 
    Class="_141_HelloWorldWebService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Threading;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class _141_HelloWorldWebService  : WebService {

  [WebMethod]
  public string HelloWorld(int callDuration) 
  {
    Thread.Sleep(callDuration);
    return String.Format(
      "Hello World from WebService. Call Time: {0}",
      callDuration);
  }    
}

答えはサービスプロキシを定義することです。と言っても、自力でコードを書く必要はなく、Visual Studio を使ってサービス参照を追加するか、または、SDK にある Wsdl.exe ツールを利用してサービスプロキシクラスを自動生成すれば OK です。ここでは、後者の Wsdl.exe ツールを利用した例を書きます。

Wsdl.exe ツールの詳しい使用方法は、MSDN ライブラリの XML Web サービス プロキシの作成 にあります。これを参考に、上記の Web サービスの URL とプロキシのコードの出力先のファイル名をパラメータとして、コマンドラインから Wsdl.exe を実行します。以下の画像のような感じです。

wsdl.exe を使ってプロキシクラスを生成

この結果、上記 (1) の Web サービスからプロキシクラス HelloWorldWebService.cs が自動生成されます。コードは以下のとおりです(一部省略、改行等をしています)。

(2) Web サービスプロキシ (HelloWorldWebService.cs)

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;

[System.CodeDom.Compiler.GeneratedCodeAttribute(
  "wsdl", "2.0.50727.3038")]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(
  Name="_141_HelloWorldWebServiceSoap", 
  Namespace="http://tempuri.org/")]
public partial class _141_HelloWorldWebService : 
  System.Web.Services.Protocols.SoapHttpClientProtocol 
{
  private System.Threading.SendOrPostCallback 
    HelloWorldOperationCompleted;
    
  public _141_HelloWorldWebService() 
  {
    this.Url = 
      "http://msdntestnew/141-HelloWorldWebService.asmx";
  }
    
  public event HelloWorldCompletedEventHandler 
    HelloWorldCompleted;

  [System.Web.Services.Protocols.SoapDocumentMethodAttribute(
    "http://tempuri.org/HelloWorld", 
    RequestNamespace="http://tempuri.org/", 
    ResponseNamespace="http://tempuri.org/", 
    Use=System.Web.Services.Description.SoapBindingUse.Literal,
    ParameterStyle=
      System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
  public string HelloWorld(int callDuration) 
  {
    object[] results = this.Invoke(
        "HelloWorld", new object[] { callDuration });
    return ((string)(results[0]));
  }
    
  public System.IAsyncResult BeginHelloWorld(
    int callDuration, 
    System.AsyncCallback callback, 
    object asyncState) 
  {
    return this.BeginInvoke(
      "HelloWorld", 
      new object[] { callDuration }, 
      callback, 
      asyncState);
  }
    
  public string EndHelloWorld(System.IAsyncResult asyncResult) 
  {
    object[] results = this.EndInvoke(asyncResult);
    return ((string)(results[0]));
  }

  // ・・・中略・・・

}

上に示したプロキシクラスに、SoapHttpClientProtocol.BeginInvoke メソッドSoapHttpClientProtocol.EndInvoke メソッド をラップした BeginHelloWorld メソッドと EndHelloWorld メソッドが生成されているのが分かるでしょうか?

このファイル HelloWorldWebService.cs を、Web アプリケーションのルート直下の App_Code フォルダに置けばプロキシは使用可能になり、それに定義されている BeginHelloWorld メソッドと EndHelloWorld メソッドを利用して、以下のように非同期 HTTP ハンドラを作成できます。

(3) 非同期 HTTP ハンドラ (141-HelloWorldAsyncHandler2.ashx)

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

using System;
using System.Web;
using System.Threading;

public class _141_HelloWorldAsyncHandler2 : IHttpAsyncHandler
{
  private HttpContext _context;
  private _141_HelloWorldWebService _serviceProxy;
    
  // これが呼び出されることはない。万一呼び出されたら例外
  // をスローして自爆する
  public void ProcessRequest (HttpContext context) 
  {
    throw new InvalidOperationException();
  }
 
  public bool IsReusable 
  {
    get { return false; }
  }

  // まず最初にこのメソッドが呼ばれる。
  // cb には ASP.NET が内部で生成したコールバックメソッド
  // void OnAsyncHandlerCompletion(System.IAsyncResult)
  // のデリゲートが渡される。これが EndProcessRequest メ
  // ソッドを呼び出す。extraData には null が渡される。
  public IAsyncResult BeginProcessRequest(
    HttpContext context,
    AsyncCallback cb,
    Object extraData)
  {
    this._context = context;

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

    _serviceProxy = new _141_HelloWorldWebService();

    // 終了すると引数 cb に設定したコールバックデリゲート
    // が起動される。その中で EndProcessRequest メソッドが
    // 呼び出される。
    return _serviceProxy.BeginHelloWorld(3000, cb, null);
  }

  // 引数 ar には BeginProcessRequest メソッドの戻り値であ
  // る IAsyncResult オブジェクトが渡される。
  public void EndProcessRequest(IAsyncResult ar)
  {
    _context.Response.Write(
      "<p>EndProcessRequest:<br />" +
      " IsThreadPoolThread is " +
      Thread.CurrentThread.IsThreadPoolThread +
      "<br />" +
      " ManagedThreadId is " +
      Thread.CurrentThread.ManagedThreadId.ToString() +
      "</p>");

    string result = _serviceProxy.EndHelloWorld(ar);

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

非同期 HTTP ハンドラでは、BeginProcessRequest メソッドが最初に実行されます。引数 context には現在の HttpContext オブジェクトへの参照が、cb には ASP.NET が内部で生成したコールバックメソッドのデリゲートが、extraData には null が渡されます。

BeginProcessRequest メソッドでは、プロキシクラスを初期化して BeginHelloWorld メソッドを呼び出し、HelloWorld メソッドの非同期実行を開始します。非同期実行の開始後すぐに制御が戻って、ここまでの処理に使用していたスレッドはスレッドプールに戻されます。

BeginHelloWorld メソッドの第 1 引数は HelloWorld メソッドに渡されます(ここでは、待機時間 3000 即ち 3 秒を渡しています)。第 2 引数 cb には BeginProcessRequest メソッドの引数 cb に渡されたコールバックメソッドのデリゲートがそのままコピーされます。第 3 引数は BeginProcessRequest メソッドの戻り値、即ち EndProcessRequest メソッドの引数 ar に渡される IAsyncResult オブジェクトから、AsyncState プロパティを使って取得できるデータですが、ここでは使用しないので null としています。

ここで覚えておくべきことは、BeginHelloWorld メソッドの第 2 引数として渡されたコールバックメソッドのデリゲートが実行されると EndProcessRequest メソッドが実行されることです(逆に言えば、コールバックメソッドが実行されないとフリーズしてしまう)。

非同期処理の完了後、コールバックメソッドのデリゲートが実行され、EndProcessRequest メソッドが呼び出されます。その際、スレッドプールから空いているスレッドを取得して EndProcessRequest メソッドを実行します。この時、引数 ar には BeginProcessRequest メソッドの戻り値である IAsyncResult オブジェクトが渡されます。 EndProcessRequest メソッドの中で、EndHelloWorld メソッドを実行し、Web サービスからの応答をその戻り値から取得します。EndProcessRequest メソッドの終了後、制御がコールバックメソッドに戻って全体のタスクが終了します。

上記の非同期 HTTP ハンドラを iframe の src 属性に設定した aspx ページの例は以下の通りです。一番上の画像は、以下の aspx ページの実行結果です。

(4) aspx ページ (141-HelloWorldAsyncHandler.aspx)

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
    <iframe src="141-HelloWorldAsyncHandler2.ashx" 
      id="iframe2"
      width="400px" 
      height="200px">
    </iframe>
  </form>
</body>
</html>

Tags:

ASP.NET

HTTP ハンドラで Session を読み書き

by WebSurfer 17. January 2012 22:27

HTTP ハンドラ内では Session の読み書きができません。例えば、以下のようにすると context.Session は null になって NullReferenceException がスローされてしまいます。

string s = (string)context.Session["note"];

ご存知でしたでしょうか? 実は、自分はそのことを知ら���くて、つい先日 1 時間ぐらいハマってしまいました。(笑)

この問題を解決するには、HTTP ハンドラのクラスにマーカーインターフェースを継承させ、この HTTP ハンドラは Session へのアクセスを必要としているという印を付けてやります。

そのマーカーインターフェイスは System.Web.SessionState 名前空間 に属しており、以下のとおり 2 種類あります。

  1. IRequiresSessionState 読み取り/書き込みアクセス権を必要とすることを指定します。
  2. IReadOnlySessionState 読み取り専用アクセス権のみが必要であることを指定します。

実際の使用例は、まぁ、書かなくても分るとは思いますが、以下のような感じです(この例では IRequiresSessionState を使用)。

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

using System;
using System.Web;
using System.Web.SessionState;

public class _MyHttpHandler : IHttpHandler, IRequiresSessionState 
{    
  public void ProcessRequest (HttpContext context) 
  {
    byte[] data = (byte[])context.Session["PageResponse"];
        
    if (data != null)
    {
      context.Response.BinaryWrite(data);
      context.Session.Remove("PageResponse");
    }
    context.Response.End();
  }
 
  // 以下省略

マーカーインターフェイスはメソッドの定義を持たないので、一般的なインターフェイスを継承したときのように、継承したクラス内にメソッドを実装する必要はありません。

Tags: ,

ASP.NET

About this blog

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

Calendar

<<  June 2020  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

View posts in large calendar