2009年2月2日月曜日

RoutingのパラメータにURLを指定したかったり

前回のエントリでFlickrから画像を取得する部分のコードに違和感を持った人がいますかね? ルートの登録でもアクションの定義でも"url"って書いてるのに、使ってないじゃないか!と。 そうなんですよ。

非同期サンプル書くのにURLを渡して、そのURLに対してWebRequest(WebClientでも)で取得するのを書こうとしてたんだけど、エラーになるのでとりあえずは固定で対応という逃げの一手。 で、どういうことかというと、例えばProxy(string url)という感じでアクションを定義(前回のサンプルだとSync2とAsync3)する感じ。

ルートの定義は

      routes.MapRoute(null,
       "Proxy/{*url}",
       new { controller = "WebRequest", action = "Proxy" }
     ); 

みたいな。 ※WebRequestControllerっていうのがいたとして。

リクエストするときのURLが昨日の例だと

http://localhost/Proxy/http://farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg

みたいな。 でも、そこはURLエンコードしとかないとさ、っていうんで

http://localhost/Proxy/http%3A%2F%2Ffarm1.static.flickr.com%2F131%2F353753310_1ed04f694c_m.jpg

みたいな。 でね、これがね、ダメなの。

img.aspx

ゴルァ~!っと怒られるわけっす。 ところで、このエラーいつだれが出してるんですかね? ってことで、調べてたんだけど、たぶんRouting(コレだ!っていう資料を見つけられなかった)。 WebServerじゃないと思うんだけどどうでしょう。 だってね。ASP.NET WebFormで以下のように書くとこれはちゃんととれるんだもん。

  public partial class _Default : System.Web.UI.Page
 {
   protected void Page_Load(object sender, EventArgs e)
   {
     if (!IsPostBack)
       var url = Request.QueryString["url"];
   }
 } 

何がまずいのかな~、と。

URL Encodingに書かれてる、使っちゃいけない文字も含まれて無いし。

だけど、MVCのRoutingってcontrollerとかactionとかっていうデータを構築してくれて、GetVirtualPathで勝手にURLエンコードした文字列返してくれるじゃないっすか。

VirtualPathData.VirtualPath プロパティ (System.Web.Routing)

その辺については特に書かれてないんだけど。 でも、Routeを登録するときにワイルドカード指定できるでしょ。 ワイルドカード内のスラッシュ'/'はエンコードしないじゃないですか。 もしやと、思って、UrlEncodeしないで試してみたんですよね。 もちろん':'は使えないんで除外します。

そしたらちゃんとリクエスト受け付けてくれるんですよ('/'を2個連続はダメだけど)。 http://localhost/Proxy/http/farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg このままだとアックションで取得出来るurlが"http/"で始まるんでちょっと感じ悪い。 ので、ルートの定義をちょっと変更。

      routes.MapRoute(null,
       "Proxy/{scheme}/{*url}",
       new { controller = "WebRequest", action = "Proxy" },
       new { scheme = @"(http|https)" }
     ); 

みたいな。 アクションの定義も。

    public ActionResult Proxy(string scheme, string url){...} 

みたいな。 このルートを使うようにHtml.ActionLinkを書いて出力させると、以下のように。

  <a href="/Proxy/http/farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg/">Flickr</a>

ふむ。

UrlEncodeしないとまずい文字を含めた、場合にどうなるかを試してみるのに、このUrlの最後に"/\<'09 in オレ>"をくっつけてみる。出力結果は↓。

  <a href="/Proxy/http/farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg/%5C%3C'09%20in%20%E3%82%AA%E3%83%AC%3E">Flickr</a>

ちゃんとUrlEncodeしてくれてる。 けど、これだとまた"Bad Request"。 まぁ、'\'とか'<','>'をエンコードしてるとはいえ、使っちゃってるのがいけないんだな、と。 そこはUrlEncodeしてるんだから通して欲しいけどね。ってことで、使用不可の文字を使ってる場合は、UrlEncodeしててもBad Requestになるので気をつけよう。

  <% = Html.ActionLink("Flickr", "Proxy", "WebRequest", new {
   scheme = "http",
   url = @"farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg/'09 in オレ"
   }, null)%>

↑こんな感じで、ActionLinkやUrl.Actionを使うのがいいみたい。 って、ことで実行結果は。 img.aspx2 img.aspx3

ASP.NET MVCは関係無いね。

ASP.NET MVCで非同期リクエスト

Improve scalability in ASP.NET MVC using Asynchronous requests « Steve Sanderson’s blog まずは↑。

非同期のIHttpAsyncHandlerをMVCでも使おうよ、という話。 ソースは部分的。

で、このたびリリースされたASP.NET MVC RC(Refreshはお早めに)。 の、FuturesにAsyncControllerが含まれてる。

そりゃ~、もう気になって仕方ないよね。 とりあえずは、ソースを確認して、どういう構成で非同期実装してるのかを見ることにしたんだけど、どうにも要領を得ないな。

AsyncManager.RegisterTast

上記エントリだとアクション内でRegisterAsyncTaskを読んで、Begin/Endそれぞれのdelegateを登録するという流れなので、 RC Futuresのソースを眺めてて、パッと目につくのがAsyncManager.RegisterTast()。 名前からしてタスクを登録するメソッド。

public IAsyncResult RegisterTask(Func beginDelegate, AsyncCallback endDelegate) 

こんな宣言なのを見ると、上記エントリと同じ使い方でいいんじゃないかと思えるんだけど。 試しに書いたコードが↓。

    public void Image(string fileName)
    {
      AsyncManager.RegisterTask(cb => {
        Debug.WriteLine(string.Format("request thread:{0}={1}({2})",
          new object[] { Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread, fileName }));

        Thread.Sleep(3000);
        cb(null);
      }, delegate(IAsyncResult result) {
        Debug.WriteLine(string.Format("response thread:{0}={1}({2})",
          new object[] { Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread, fileName }));

        var dir = Server.MapPath("~/App_Data/Images");
        var path = string.Format("{0}\\{1}", dir, fileName);

        if (System.IO.File.Exists(path))
        {
          File(path, "image/png").ExecuteResult(ControllerContext);
        }
        else
          Response.StatusCode = 404;
      });
    }

Add_Data/ImagesにPNG画像ファイルを入れて、そのファイル名を指定するとファイルを返すという簡単なもの。 動くのは動く。普通に。 でも、スレッドIDはbegin/endどっちも同じものしか使われてない様子。 開発環境だからかな~。なんでかな~。 レスポンスを返す部分で、ActionResult.ExecuteResult()を呼んで、その場でResponseしちゃうっていうのはちょっと違う気がしなくもない。

Action/ActionCompleted

次にソースを追っかけて気がついた。「// Is this the Foo() / FooCompleted() pattern?」なんてコメント発見。普通にアクションを書いて、同じ名前のアクション名+Completedっていうアクションを定義する方法。わかりにく!

    public void Image2(string fileName)
    {
      Thread.Sleep(3000);

      HttpContext.Items["params"] = fileName;
      Debug.WriteLine(string.Format("request thread:{0}={1}({2})",
        new object[] { Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread, fileName}));
    }

    public ActionResult Image2Completed()
    {
      Debug.WriteLine(string.Format("response thread:{0}={1}({2})",
      
new object[] { Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread, HttpContext.Items["params"]
}));

      var dir = Server.MapPath("~/App_Data/Images");
      var path = string.Format("{0}\\{1}", dir, HttpContext.Items["params"]);

      if (System.IO.File.Exists(path))
        return File(path, "image/png");
     
      Response.StatusCode = 404;
      return new EmptyResult();
    }

ようするに↑こうなんだけど。 んで、これだと、スレッドIDが変わることがあるから、なんか上手く行ってる気がしなくもない。 なんせ、リクエストを受け付けたときのパラメータがCompletedの方には渡されないから、HttpContext.Itemに入れてるのが、かなり自信ない。 でも、この書き方だと、Completedの戻り値がActionResultだから分かりやすいんじゃないかと思える。 スレッドIDも違うし。

BeginAction/EndAction

さらにコードを追いかけてると、今度は「// Is this the BeginFoo() / EndFoo() pattern?」と書かれてる。 ってことは、アクション名にBeginなんちゃら/Endなんちゃらってかいておくと、それを呼び出してくれるのかなと。ソースもAsyncActionMethodSelectorだし。 で、試した。 今度はBegin/Endのパラメータ指定に制限があって、Beginは戻り値IAsyncResultで引数AsyncCallback callback, object stateが必須。EndにはIAsyncResult resultが必須。

    public IAsyncResult BeginImage3(AsyncCallback callback, object state)
    {
      Debug.WriteLine(string.Format("request thread:{0}={1}({2})",
        new object[] { Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread, "test" }));

      var web = WebRequest.Create("http://farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg");
      HttpContext.Items["web"] = web;
      return web.BeginGetResponse(callback, state);
    }

    public void EndImage3(IAsyncResult result)
    {
      Debug.WriteLine(string.Format("response thread:{0}={1}({2})",
        new object[] { Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread, "test" }));

      var web = HttpContext.Items["web"] as WebRequest;
      WebResponse res = web.EndGetResponse(result);

      Response.ContentType = res.ContentType;

      var reader = new BinaryReader(res.GetResponseStream());
      Response.BinaryWrite(reader.ReadBytes((int)res.ContentLength));
    } 

追記 ここはHttpContext.Item使わなくてもBeginGetResponseにstateの代わりにweb渡せばEndAsync3でresult.AsyncStateでとれますね。

今度はFlickrの画像をWebRequestで取得するように変更。 戻り値が必要だし。 んで、デバッグ出力のスレッドIDはBeginとEndでちゃんと違う。 そんなこんなで、どうやって書くのが正しいのかいまいちよく分からないAsyncController。 ちなみにRouteは↓こんな感じで書いてます。

      routes.MapAsyncRoute(null,
        "Async3/{*url}",
        new { controller = "AsyncImages", action = "Image3" }
      );

      routes.MapAsyncRoute(null,
        "Async2/{fileName}",
        new { controller = "AsyncImages", action = "Image2" }
      );

      routes.MapAsyncRoute(null,
        "Async/{fileName}",
        new { controller = "AsyncImages", action = "Image" }
      ); 

悩ましいな~。なんて思ってた所に新たな非同期エントリ登場。

Extend ASP.NET MVC for Asynchronous Action - Happy Coding

これまた全然違う作り方してるんだよね。

Controllerはそのままに、AsyncMvcHandler実装(他にもいろいろあるけど)で乗り切る方法。 これだとControllerFactoryもいじる必要無いし、アクション単位でAsyncAction属性指定で非同期判別。 戻り値もActionResultだし。

サンプルコードが↓こんなの。

        [AsyncAction]
        public ActionResult AsyncAction(AsyncCallback asyncCallback, [AsyncState]object asyncState)
        {
          
SqlConnection conn = new SqlConnection("Data
Source=.\\sqlexpress;Initial Catalog=master;Integrated
Security=True;Asynchronous Processing=true");
            SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
            conn.Open();
           
            return this.Async(
                cmd.BeginExecuteNonQuery(asyncCallback, asyncState),
                (ar) =>
                {
                    int value = cmd.EndExecuteNonQuery(ar);
                    conn.Close();
                    return this.View();
                });
        }

this.AsyncっていうのがControllerの拡張メソッド。これでEndProcessRequestの時に呼び出すdelegateを指定。スゴイよね。 コード量も少ないし。delegateの戻りがそのままアクションの戻りとして使われるし。 なんかコードみてるとasyncCallbackとasyncStateをCallContextに入れるようになってる。初めて見た。 コレだとスレッド単位のコンテキストオブジェクト管理が出来るっぽい。

ActionResultなんかはHttpContext.Itemに入れて、リクエストコンテキストで管理。 ちなみにコレのプロジェクトにベンチマーク用のコードが入っててこれいいじゃん!みたいな。 PowerShellとIIS6リソースキットに含まれるTinyGetを使って時間を計測。 シンプルで簡単なテストだから試してみた。

試すに当たって使った同期版のコードは↓。

      public ActionResult Sync(string fileName)
      {
        Thread.Sleep(3000);

        var dir = Server.MapPath("~/App_Data/Images");
        var path = string.Format("{0}\\{1}", dir, fileName);

        if (System.IO.File.Exists(path))
          return File(path, "image/png");

        return new EmptyResult();
      }

      public ActionResult Sync2(string url)
      {
        var web = new WebClient();

        var bytes = web.DownloadData("http://farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg");
        return File(bytes, web.ResponseHeaders["Content-Type"]);
      } 

Sync vs Async(Imageアクション)

Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Sync/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Async/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Sync 21.33秒、Async 14.68秒。

Sync vs Async2(Image2アクション)

Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Sync/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Async2/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Sync 21.40秒、Async 12.70秒。

ここまでの2個のテストではそれぞれ3秒のウェイトを入れてこの時間だから、非同期でリクエストスレッドを早く開放したほうが次のリクエストを続々と受け入れることが出来るようになるんだから、最終的にこのくらいの差が出てくるのも納得(だよね?)。

Sync2vs Async3(Image3アクション):Flickrから取得

Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Sync2/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Measure-Command {.\tinyget -srv:localhost -r:53976 -uri:/Async3/Cart.png -threads:50 -loop:1} [System.Threading.Thread]::Sleep(2000) Sync 3.84秒、Async 2.16秒。

コレに関してはあんまり意味ないね。Flickrからの取得にかかる時間が毎回一定なわけじゃないし。 Any good ideas to build an async action for scalability improvement? - ASP.NET Forums ここで、それぞれの設計について話をしてる。

OnAction~フィルターの動作も非同期にしないとっていう話? ふむ~。 じきにAsyncControllerの使い方を公開するってことなんでそれまで待つのがいいかもね。

2026年06月17日の記事一覧

(全 13 件) Foundry Toolkit for Visual Studio Code 自分のPCで「とりあえず動く」ではなく「一番賢く使える」ローカルLLMを教えてくれる「whichllm」、実測ベンチでランク付け(生成AIクローズアップ) | ...