WebSurfer's Home

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

非同期 HTTP ハンドラ

by WebSurfer 2012年9月23日 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

ASP.NET AJAX でページの静的メソッド呼び出し

by WebSurfer 2012年8月18日 16:32

ASP.NET の AJAX 機能を使って、ブラウザからクライアントスクリプトでサーバー側のメソッドを呼び出し、データを受け取って表示する方法を、先の記事 ASP.NET AJAX と Web サービス で紹介しました。

その例では、呼び出す相手は Web サービス (.asmx) のメソッドでしたが、それ以外に、自分のページ (.aspx) の静的メソッドを呼び出すことも可能です。

そのためのポイントは、簡単に書くと以下の 3 点です。

  1. ScriptManager で EnablePageMethods を true に設定。
  2. クライアントスクリプトから呼び出すサーバー側のメソッドは public static にして WebMethodAttribute 属性を付与。
  3. クライアントスクリプトからは、プロキシクラス PageMethods を通じて呼び出し。プロキシクラス PageMethods のコードはサーバーで自動生成され、html ソースにインラインで含まれてブラウザに送信されます。(呼び出し先が Web サービスの場合は WebService.asmx/js というような外部ファイルになります)

詳しくは、MSDN ライブラリの クライアント スクリプトへの Web サービスの公開 の「ASP.NET Web ページ内の静的メソッドの呼び出し」というセクションを参照してください。

サンプルコードは上に紹介した MSDN ライブラリのページにもありますが、先の記事 ASP.NET AJAX と Web サービス で書いた Web サービスの例を、Page の静的メソッドを使う方法に置き換えたサンプルコードを以下に書いておきます。

(1) Web ページ (173-ASPNETAjaxAndStaticMethod.aspx)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="System.Web.Services" %>

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

<script runat="server">

    public class Car
    {
        public string Make;
        public string Model;
        public int Year;
        public int Doors;
        public string Colour;
        public float Price;
    }

    static protected List<Car> Cars = new List<Car>{
        new Car{Make="Audi",Model="A4",Year=1995,
            Doors=5,Colour="Red",Price=2995f},
        new Car{Make="Ford",Model="Focus",Year=2002,
            Doors=5,Colour="Black",Price=3250f},
        new Car{Make="BMW",Model="5 Series",Year=2006,
            Doors=4,Colour="Grey",Price=24950f},
        new Car{Make="Renault",Model="Laguna",Year=2000,
            Doors=5,Colour="Red",Price=3995f},
        new Car{Make="Toyota",Model="Previa",Year=1998,
            Doors=5,Colour="Green",Price=2695f},
        new Car{Make="Mini",Model="Cooper",Year=2005,
            Doors=2,Colour="Grey",Price=9850f},
        new Car{Make="Mazda",Model="MX 5",Year=2003,
            Doors=2,Colour="Silver",Price=6995f},
        new Car{Make="Ford",Model="Fiesta",Year=2004,
            Doors=3,Colour="Red",Price=3759f},
        new Car{Make="Honda",Model="Accord",Year=1997,
            Doors=4,Colour="Silver",Price=1995f}
    };

    [WebMethod]
    public static List<Car> GetCarsByDoors(int doors)
    {
        var query = from c in Cars
                    where c.Doors == doors
                    select c;

        return query.ToList();
    }    
    
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>ASP.NET AJAX to call static method</title>
  <script src="Scripts/jquery-1.4.1.js" type="text/javascript">
  </script>
</head>
<body>
  <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" 
      runat="server" 
      EnablePageMethods="True">
      <Scripts>
        <asp:ScriptReference 
          Path="~/173-ASPNETAjaxAndStaticMethod.js" />
      </Scripts>
    </asp:ScriptManager>
    <div>
      Number of doors: 
      <asp:DropDownList ID="ddlDoors" runat="server">
        <asp:ListItem>2</asp:ListItem> 
        <asp:ListItem>3</asp:ListItem>
        <asp:ListItem>4</asp:ListItem>
        <asp:ListItem>5</asp:ListItem>
      </asp:DropDownList>   
    </div>
    <input 
      type="button" 
      id="Button1" 
      value="Get Cars" 
      onclick="getCars($('#<%=ddlDoors.ClientID%>').val());" /> 
    <div id="output"></div>
  </form>
</body>
</html>

(2) JavaScript (173-ASPNETAjaxAndStaticMethod.js)

var serviceProxy;

// プロキシの初期化
function pageLoad() {
    serviceProxy = new PageMethods();
}

// ボタンクリックで呼び出されるメソッド  
function getCars(doors) {
    serviceProxy.GetCarsByDoors(doors, Succeeded, Failed);
}

// AJAX 通信が成功したときに呼び出され、戻ってきたデータ
// を処置するコールバック関数。
// 引数 cars は JSON 文字列ではなく、パース済みのオブジェ
// クト。.NET 3.5 で追加された d パラメータはプロキシで
// 適切に処置されるらしい。
function Succeeded(cars) {
    $('#output').empty();
    $.each(cars, function (index, car) {
        $('#output').append(
            '<p><strong>' + car.Make + ' ' +
            car.Model + '</strong><br /> Year: ' +
            car.Year + '<br />Doors: ' +
            car.Doors + '<br />Colour: ' +
            car.Colour + '<br />Price: £' +
            car.Price + '</p>');
    });
}

// 通信に失敗したとき呼び出されるコールバック関数。 
function Failed(error, userContext, methodName) {
    if (error !== null) {
        var msg = "An error occurred: " +
            error.get_message();
        $('#output').text(msg);
    }
}

if (typeof (Sys) !== "undefined") {
    Sys.Application.notifyScriptLoaded();
}

なお、上記 Web ページの静的メソッド GetCarsByDoors は jQuery.ajax からも呼び出せます。先の記事 jQuery AJAX と Web サービス で紹介したコードで、呼び出し先 (url) を変更するだけです。呼び出し先の Web ページの URL が上の例のように 173-ASPNETAjaxAndStaticMethod.aspx とすると、以下のような感じです。

// 前略 
function getCars() {
  $.ajax({
    type: "POST",
    url: "173-ASPNETAjaxAndStaticMethod.aspx/GetCarsByDoors",
    data: "{doors: " + $('#ddlDoors').val() + "}",
// 後略

その場合、上で述べた ScriptManager は不要ですし、プロキシにアクセスするスクリプト(上の例で言うと 173-ASPNETAjaxAndStaticMethod.js ファイル)も不要です。それゆえ、呼び出し元は .aspx ページである必要はなく、静的な .html ページとしても OK です。

Tags:

AJAX

WebBrowser で HttpOnly 属性の Cookie 取得

by WebSurfer 2012年8月13日 15:35

.NET Framework の WebBrowser を利用した Windows アプリで、ドキュメントに関連付けられている HTTP Cookie を取得するには HtmlDocument.Cookie プロパティ が利用できます。

ただし、HttpOnly 属性を持つ HTTP Cookie は例外です。その理由は、The Code Project のページ Retrieve HttpOnly Session Cookie in WebBrowser に述べられていますが、IE6 以降でクロスサイトスクリプティング対応のため HttpOnly 属性が追加され、その属性を持つ HTTP Cookie にはクライアントスクリプトからアクセスできなくなっているからだそうです。

HttpOnly 属性を持つ HTTP Cookie には、例えば、ASP.NET のセッションクッキー、匿名ユーザー識別用クッキーが該当します(下の Fiddler による応答ヘッダーの画像で、名前が ASP.NET_SessionId と .ASPXANONYMOUS のもの)。

HTTP Cookies(Fiddler による応答ヘッダー)

上の画像の HTTP Cookie の内、HtmlDocument.Cookie プロパティで取得できるのは RandomNumber と DateTimeCookie のみです。この例では、HtmlDocument.Cookie プロパティから取得できる文字列は以下のようになります。

DateTimeCookie=2012/08/13 14:22:48; RandomNumber=1123940529

すべての HTTP Cookie を取得するには、WININET.dll の InternetGetCookieExA 関数 (wininet.h) を利用できます(mshtml.dll ではない点に注意)。以下のような感じです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WebBrowserHttpOnlyCookie
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();

      textBox1.Text = 
          "http://msdntestnew/171-SendCookies.aspx";
    }

    private void button1_Click(object sender, EventArgs e)
    {
      webBrowser1.Navigate(textBox1.Text);
    }

    [DllImport("wininet.dll", CharSet = CharSet.Auto, 
      SetLastError = true)]
    static extern bool InternetGetCookieEx(
        string pchURL, 
        string pchCookieName, 
        StringBuilder pchCookieData, 
        ref uint pcchCookieData, 
        int dwFlags, 
        IntPtr lpReserved);

    const int INTERNET_COOKIE_HTTPONLY = 0x00002000;

    public static string GetCookies(string uri)
    {
      uint datasize = 1024;           
      StringBuilder cookieData = 
        new StringBuilder((int)datasize);
      bool result = InternetGetCookieEx(
                         uri,
                         null,
                         cookieData,
                         ref datasize,
                         INTERNET_COOKIE_HTTPONLY,
                         IntPtr.Zero);

      if (result && cookieData.Length > 0)
      {
        return cookieData.ToString();
      }
      else
      {
        return null;
      }
    }

    private void webBrowser1_DocumentCompleted(
      object sender, 
      WebBrowserDocumentCompletedEventArgs e)
    {
      label1.Text = webBrowser1.Document.Cookie;
      label2.Text = GetCookies(textBox1.Text);
    }
  }
}

Tags: , ,

.NET Framework

About this blog

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

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar