はじめに
最近はWebサービスやRSSフィードが特別なことでなくなり、どこか他の場所からデータを収集することが当たり前となっているようです。幸い、ASP.NETのおかげで、リモートサーバーからHTTPを介してデータを取得することは簡単に行えるようになっています(もしかしたら、簡単すぎると言えるかもしれません)。多くのアプリケーションがこれを利用しているのは、1つの要求を受け取るたびに複数の要求が出されるからです。
私がリモートサーバーからデータを取得して表示する必要があると初めて思ったのは、ある顧客のためにWebサイトを作成していて、その顧客がホームページに株価情報を表示したいと言ってきたときでした。彼らを失望させたくなかったので(何しろ、株式を公開するほど大きな顧客だったのです!)、どう取り組むべきか厳密に考えてみることにしました。そして、どうにかスクリプトを動かすことに成功しましたが(簡単な作業ではありませんでした――Internet Transfer ControlとWinInetのことをご記憶の方にはご理解いただけるでしょう)、その遅さにはがっかりしました。この遅さに加え、要求が出されたときWebサーバーがクラッシュしがちなことや、当時はダイヤルアップを通じてまだ多くの作業を行っていたことなどの理由から、株価情報をかなり長い期間にわたってキャッシュするようにしていました。
HTTPを介してリモートサーバーのデータを取得する
当時、私はHTTPデータを取得するために.NET以前のASPで何年も同じスクリプトを使用していて特別困ったことはありませんでした。そのため、.NET版を書くことになったとき、現在のスクリプトを書き換えてそのまま使えばよいだろうと考えていました。
.NET以前のASPに興味がある方は、数年前に書いた短い記事『A Simple Method for Caching HTTP Requests』をご覧ください。
ところで、当のスクリプトには変更すべき部分が少なからずあることが判明しました。COM Componentの代わりに.NET Frameworkを使うことになったのでHTTPセクションの全面変更が必要でした。.NETにキャッシュ機能が組み込まれたのでメモリキャッシュのためにApplication変数を使うことも必要なくなりました。以前のスクリプトを書き換えるだけではもう無理で、そろそろ最初から作り直さなければならないようでした。
最初のステップは、当然ながら、リモートサーバーからHTTPを介してデータを取得することです。このプロセスは実際それほど複雑でありませんが、多数のオブジェクトが関係するので複雑に見えるかもしれません。
<%@ Page Language="VB" %> <%@ Import Namespace="System.Net" %> <%@ Import Namespace="System.IO" %> <script runat="server"> Sub Page_Load(Src as object, E as EventArgs) Dim strRssFeedText As String ' Call the GetWebPageAsString function to retrieve ' the 15 Seconds RSS feed as text. strRssFeedText = GetWebPageAsString( _ "http://www.15seconds.com/dyna/15secs_rss.asp") ' HTMLEncode the RSS feed and display it in the browser. litTextRetrieved.Text = Server.HTMLEncode(strRssFeedText) End Sub Function GetWebPageAsString(strURI As String) As String ' Declare our variables. Dim objWebRequest As WebRequest Dim objWebResponse As WebResponse Dim objStream As Stream Dim objStreamReader As StreamReader Dim strResponseText As String ' Create a new request. objWebRequest = WebRequest.Create(strURI) ' Get the response to our request as a stream object. objWebResponse = objWebRequest.GetResponse() objStream = objWebResponse.GetResponseStream() ' Create a stream reader to read the data from the stream. objStreamReader = New StreamReader(objStream) ' Copy the text retrieved from the stream to a variable. strResponseText = objStreamReader.ReadToEnd() ' Close our objects. objStreamReader.Close objStream.Close objWebResponse.Close ' Set return value. GetWebPageAsString = strResponseText End Function </script> <html> <head> <title>HTTP Request Sample</title> </head> <body> <pre> <asp:Literal id="litTextRetrieved" runat="server" /> </pre> </body> </html>
上記のスクリプトを実行すると、http://www.15seconds.com/dyna/15secs_rss.aspに対して要求が出されます(ここではサイト「15 Seconds」のRSSフィードのアドレスを使用しています)。このフィードの一番下にサーバーの現在時刻を示すコメントがあり、大抵数分ずれています。
HTTPを介してリモートサーバーのデータをキャッシュする
キャッシュがどれほど偉大な技術かを初めて知ったのは、古いコンピュータをアップグレードしようとしたときでした。私は新しいディスクドライブを手に入れ、それまでのデータをコピーするために、古いコンピュータに追加装着しました。古い起動ディスクを使ってマシンを起動し、XCopyを用いてファイルをドライブ間で転送したのですが、驚くほど処理が遅いのです。あまりに遅いので、新しいディスクドライブに問題があるに違いないと考えました。数時間かけて問題を探り、何件か電話した後、SMARTDriveが高速化に非常に大きな貢献をしていることを発見しました。以来、私はキャッシュの熱烈な愛好者になりました。
HTTP要求をキャッシュすることがなぜ重要なのでしょうか。HTTP要求は、Webサーバーに対する要求の中でも特に遅いからです。HTTP要求は大抵のデータベース要求よりも時間がかかります。データベースはローカルディスクに置かれるのが一般的ですが、HTTP要求はインターネットを流れ、失敗の可能性を含む多数の処理を経てやっと応答が届きます。まず、DNS経由で接続する相手のサーバーのIPアドレスを解決する必要があります。次に、要求をサーバーに到達させる必要があります。その後、サーバーが応答し、その応答を最終的に受け取る必要があります。我々はインターネットの信頼性が向上したおかげで多数のステップを経てHTTP要求が成功するのは当たり前と思っていますが、応答が到達するまでには多くの処理を経る必要があり、それらは必ずしも瞬時に起こるとは限りません。
<%@ Page Language="VB" %> <%@ Import Namespace="System.Net" %> <%@ Import Namespace="System.IO" %> <script runat="server"> Sub Page_Load(Src as object, E as EventArgs) Dim strRssFeedText As String ' Retrieve the entry from the cache strRssFeedText = Cache("15SecsRssFeedText") ' If the data retrieved is null then we ' get it from the remote server and insert ' it into the cache for future calls. If strRssFeedText = Nothing Then ' Call the GetWebPageAsString function to retrieve ' the 15 Seconds RSS feed as text. strRssFeedText = GetWebPageAsString( _ "http://www.15seconds.com/dyna/15secs_rss.asp") ' Insert the text into the cache. I'm setting it ' to expire 15 minutes from when we add it. Cache.Insert("15SecsRssFeedText", strRssFeedText, _ Nothing, DateTime.Now.AddMinutes(15), _ System.Web.Caching.Cache.NoSlidingExpiration) End If ' HTMLEncode the RSS feed and display it in the browser. litTextRetrieved.Text = Server.HTMLEncode(strRssFeedText) End Sub Function GetWebPageAsString(strURI As String) As String ' Declare our variables. Dim objWebRequest As WebRequest Dim objWebResponse As WebResponse Dim objStream As Stream Dim objStreamReader As StreamReader Dim strResponseText As String ' Create a new request. objWebRequest = WebRequest.Create(strURI) ' Get the response to our request as a stream object. objWebResponse = objWebRequest.GetResponse() objStream = objWebResponse.GetResponseStream() ' Create a stream reader to read the data from the stream. objStreamReader = New StreamReader(objStream) ' Copy the text retrieved from the stream to a variable. strResponseText = objStreamReader.ReadToEnd() ' Close our objects. objStreamReader.Close objStream.Close objWebResponse.Close ' Set return value. GetWebPageAsString = strResponseText End Function </script> <html> <head> <title>Cached HTTP Request Sample</title> </head> <body> <pre> <asp:Literal id="litTextRetrieved" runat="server" /> </pre> </body> </html>
上記リストの太字の行に注目してください。これらの行がキャッシュの処理に関係します。1行目で、キャッシュ内のデータを取得します。2行目で、戻り値のデータがあるかを確認します。戻り値のデータがある場合は、そのデータを用いてページを作成し、リモートサーバーにはHTTP要求を出しません。データがキャッシュにない場合は、リモートサーバーにデータを要求します。3行目で、将来のページ要求のためにデータをキャッシュに保存します。
ASP.NETのキャッシュエンジンは堅牢にできており、キャッシュ項目の有効期限を何通りもの方法で指定できます。そのせいでCache.Insert
メソッドは複雑に見えます。上の例では次の構文のメソッドを使用しています。
Insert(key As String, value As Object, dependencies As CacheDependency, _ absoluteExpiration As DateTime, slidingExpiration As TimeSpan)
引数key
とvalue
の意味はすぐに分かるでしょう。key
はキャッシュするデータの識別情報で、value
はキャッシュする実際のデータです。dependencies
は、キャッシュを別のオブジェクト(通常はファイル)に依存させる場合に使用します。今回は使用していないのでNothingです。absoluteExpiration
では、キャッシュの期限を指定します。上の例では、キャッシュ後15分でデータを期限切れにするように設定してあります。slidingExpiration
は、一定期間使われない場合に限ってキャッシュからデータを削除するよう指示するものです。キャッシュが使われるたびにリセットされるカウントダウンタイマのようなものです。リセットされずにゼロになったとき初めてキャッシュが削除されます。
Cache
オブジェクトのInsert
メソッドの詳細については、MSDNドキュメントのCache.Insert
メソッドの説明を参照してください。このメソッドはオーバーロードされ、今回の例では第3版(有効期限を設定できるバージョン)を使用しています。
キャッシュ版のスクリプトを実行すると、サイト「15 Seconds」のRSSフィードが再び表示されます。今回は、更新しても一番下のタイムスタンプは変化しません。毎回新しいデータを取得するのではなく、キャッシュされたデータを表示しているからです。しかし、15分待ってからページを更新すると、新しいタイムスタンプが取得され、次の15分間はこれが返されます。
結論
ASP.NETではオブジェクトを簡単にキャッシュできることが分かりました。この強力な組み込みキャッシュ機能をいろいろな場面で使うとよいと思いますが、中でもHTTP要求については、ぜひこれでキャッシュすることをお勧めします。サイトが高速になるだけでなく、ネットワークトラフィックが削減され、リモートサーバの負荷も減ります。さて、残る問題はキャッシュの有効期限をどれくらいに設定するかです。これはキャッシュの内容に依存し、キャッシュごとに変化しますが、1分程度キャッシュするだけでも多忙なサイトでは大きな効果があります。