Text/Tao Gang
Web アプリケーションで大きなファイルのダウンロードを処理することは、常に困難であることで悪名高く、そのため、ダウンロードが中断されると、ほとんどのサイトでユーザーに不幸が降りかかります。ただし、ASP.NET アプリケーションで大きなファイルの再開可能 (継続可能) ダウンロードをサポートできるようにすることができるため、今はその必要はありません。この記事で説明する方法を使用すると、ダウンロード プロセスを追跡できるため、従来の ISAPI ダイナミック リンク ライブラリやアンマネージ C++ コードを必要とせずに、動的に作成されたファイルを処理できます。
クライアントがインターネットからファイルをダウンロードするサービスを提供するのが最も簡単ですよね。ダウンロード可能なファイルを Web アプリケーション ディレクトリにコピーし、リンクを公開するだけで、関連するすべての作業を IIS に実行させることができます。ただし、ファイル サービスは単なる面倒な作業であってはなりません。世界中の人が自分のデータにアクセスできるようにしたくないし、サーバーが何百もの静的ファイルで詰まるのは望ましくありません。一時ファイルをダウンロードする必要さえありません - これらのファイルは、クライアントがダウンロードを開始した後のアイドル時間中にのみ作成されます。
残念ながら、ダウンロード要求に対する IIS の既定の応答を使用してこれらの効果を実現することはできません。したがって、一般に、ダウンロード プロセスを制御するには、開発者はカスタム .aspx ページにリンクしてユーザーの資格情報を確認し、ダウンロード可能なファイルを作成し、次のコードを使用してファイルがクライアントにプッシュされるようにする必要があります
。
Response.End()
ここで本当の問題が発生します。
何が問題ですか?
WriteFile メソッドは完璧に見えます。ファイルのバイナリ データがクライアントに送信されます。しかし、私たちが最近まで知らなかったことは、WriteFile メソッドは悪名高いメモリを大量に消費し、ファイル全体をサーバーの RAM にロードして提供することです (実際、ファイルの 2 倍のサイズを必要とします)。大きなファイルの場合、これによりサービス メモリの問題が発生し、ASP.NET プロセスが重複する可能性があります。しかし、2004 年 6 月に、Microsoft は問題を解決するパッチをリリースしました。このパッチは、.NET Framework 1.1 Service Pack (SP1) の一部になりました。
このパッチでは、ディスク ファイルをより小さいメモリ バッファに読み取り、ファイルの転送を開始する TransmitFile メソッドが導入されています。この解決策はメモリとループの問題を解決しますが、まだ満足のいくものではありません。応答ライフサイクルを制御することはできません。ダウンロードが正しく完了したかどうかを知る方法はなく、ダウンロードが中断されたかどうかを知る方法もありません。また、(一時ファイルを作成した場合は) ファイルを削除すべきかどうか、いつ削除すべきかを知る方法もありません。さらに悪いことに、ダウンロードが失敗した場合、TransmitFile メソッドはクライアントが次に試行するファイルの先頭からダウンロードを開始します。
考えられる解決策の 1 つであるバックグラウンド インテリジェント転送サービス (BITS) の実装は、クライアント ブラウザーとオペレーティング システムの独立性を維持するという目的を損なうため、ほとんどのサイトでは実現できません。
満足のいく解決策の基礎は、WriteFile によって引き起こされるメモリ混乱の問題を解決するという Microsoft の最初の試みに基づいています (ナレッジベースの記事 812406 を参照)。この記事では、ファイル ストリームからデータを読み取るインテリジェントなチャンク データ ダウンロード プロセスについて説明しました。サーバーはバイト チャンクをクライアントに送信する前に、Response.IsClientConnected プロパティを使用してクライアントがまだ接続されているかどうかを確認します。接続がまだ開いている場合はストリーム バイトの送信を続けますが、そうでない場合はサーバーが不要なデータを送信しないように停止します。
これは、特に一時ファイルをダウンロードする場合に採用されるアプローチです。 IsClientConnected が False を返した場合は、ダウンロード プロセスが中断されたことがわかり、ファイルを保存する必要があります。それ以外の場合は、プロセスが正常に完了したときに一時ファイルを削除します。さらに、中断されたダウンロードを再開するには、最後のダウンロード試行中にクライアント接続が失敗した時点からダウンロードを開始するだけです。
HTTP プロトコルとヘッダー情報 (Header) のサポート
HTTP プロトコルのサポートを使用して、中断されたダウンロードのヘッダー情報を処理できます。少数の HTTP ヘッダーを使用すると、HTTP プロトコル仕様に完全に準拠するようにダウンロード プロセスを強化できます。この仕様は、範囲とともに、中断されたダウンロードを再開するために必要なすべての情報を提供します。
仕組みは次のとおりです。まず、サーバーがクライアント側で再開可能なダウンロードをサポートしている場合、初期応答で Accept-Ranges ヘッダーを送信します。サーバーは、一意の識別文字列を含むエンティティ タグ ヘッダー (ETag) も送信します。
以下のコードは、最初のダウンロード要求に応じて IIS がクライアントに送信するヘッダーの一部を示しています。ヘッダーは、要求されたファイルの詳細をクライアントに渡します。
HTTP/1.1 200 OK
接続: 閉じる
日付: 2004 年 10 月 19 日火曜日 15:11:23 GMT
受け入れ範囲: バイト
最終更新日: 2004 年 9 月 26 日 (日) 15:52:45 GMT
Eタグ: "47febb2cfd76c41:2062"
キャッシュ制御: プライベート
コンテンツ タイプ: application/x-zip-compressed
Content-Length: 2844011
これらのヘッダー情報を受信した後、ダウンロードが中断された場合、IE ブラウザーは後続のダウンロード リクエストで Etag 値と Range ヘッダー情報をサーバーに送り返します。以下のコードは、中断されたダウンロードを再開しようとするときに IE がサーバーに送信するヘッダーの一部を示しています。
GET
これらのヘッダーは、IE が IIS によって提供されたエンティティ タグをキャッシュし、それを If-Range ヘッダーでサーバーに送り返したことを示します。これは、ダウンロードがまったく同じファイルから再開されることを保証する方法です。残念ながら、すべてのブラウザが同じように動作するわけではありません。ファイルを検証するためにクライアントによって送信されるその他の HTTP ヘッダーには、If-Match、If-Unmodified-Since、Unless-Modified-Since などがあります。明らかに、クライアント ソフトウェアがどのヘッダーをサポートする必要があるか、またはどのヘッダーを使用する必要があるかについて、仕様は明示的ではありません。したがって、一部のクライアントはヘッダー情報をまったく使用しませんが、IE は If-Range と Unlimited-Modified-Since のみを使用します。この情報はコードで確認した方がよいでしょう。このアプローチを採用すると、アプリケーションは非常に高いレベルで HTTP 仕様に準拠し、さまざまなブラウザーで動作することができます。 Range ヘッダーは要求されたバイト範囲を指定します。この場合、それはサーバーがファイル ストリームを再開する開始点になります。
IIS は、ダウンロードの再開の要求タイプを受信すると、次のヘッダー情報を含む応答を送り返します:
HTTP/1.1 206 部分コンテンツ
コンテンツ範囲: バイト 822603-2844010/2844011
受け入れ範囲: バイト
最終更新日: 2004 年 9 月 26 日 (日) 15:52:45 GMT
Eタグ: "47febb2cfd76c41:2062"
キャッシュ制御: プライベート
コンテンツ タイプ: application/x-zip-compressed
コンテンツの長さ: 2021408
上記のコードの HTTP 応答は元のダウンロード リクエストとは若干異なることに注意してください。元のダウンロード リクエストが 200 であるのに対し、ダウンロードを再開するリクエストは 206 です。これは、ネットワーク上で渡されているものが部分的なファイルであることを示しています。今回は、Content-Range ヘッダーは、渡されるバイトの正確な数と位置を示します。
IE はこれらのヘッダー情報を非常に重視します。最初の応答に Etag ヘッダー情報が含まれていない場合、IE はダウンロードを再開しようとしません。私がテストした他のクライアントは ETag ヘッダーを使用せず、単にファイル名とリクエスト スコープに依存し、ファイルを検証しようとする場合は Last-Modified ヘッダーを使用します。
HTTP プロトコルの深い理解
前のセクションで示したヘッダー情報は、ダウンロードを再開するソリューションを機能させるのに十分ですが、HTTP 仕様を完全にカバーしているわけではありません。
Range ヘッダーは、1 つのリクエストで複数の範囲を要求できます。これは「マルチパート範囲」と呼ばれる機能です。分割ダウンロードと混同しないでください。ほとんどすべてのダウンロード ツールは、ダウンロード速度を上げるために分割ダウンロードを使用します。これらのツールは、それぞれが異なる範囲のファイルを要求する 2 つ以上の同時接続を開くことでダウンロード速度を向上させると主張しています。
マルチパート範囲の考え方では複数の接続は開かれませんが、クライアント ソフトウェアが 1 回の要求/応答サイクルでファイルの最初の 10 バイトと最後の 10 バイトを要求できるようになります。
正直に言うと、この機能を使用するソフトウェアを見つけたことがありません。ただし、コード宣言に「HTTP に完全に準拠していない」と書くことは拒否します。この機能を省略すると、間違いなくマーフィーの法則に違反します。いずれにせよ、マルチパート範囲は電子メール送信でヘッダー情報、プレーン テキスト、添付ファイルを分離するために使用されます。
サンプル コード
私たちは、クライアントとサーバーがヘッダー情報を交換してダウンロードを再開できるようにする方法を理解しています。この知識とファイル ブロック ストリーミングのアイデアを組み合わせることで、ASP.NET アプリケーションに信頼性の高いダウンロード管理機能を追加できます。
ダウンロード プロセスを制御する方法は、クライアントからのダウンロード リクエストをインターセプトし、ヘッダー情報を読み取り、適切に応答することです。 .NET が登場する前は、この機能を実装するには ISAPI (Internet Server API) アプリケーションを作成する必要がありましたが、.NET Framework コンポーネントには IHttpHandler インターフェイスが用意されており、これをクラスに実装すると、.NET コードのみを使用してこれを実行できるようになります。そしてリクエストを処理します。これは、アプリケーションがダウンロード プロセスを完全に制御し、応答することができ、IIS 自動化機能が関与したり、使用されたりすることがないことを意味します。
サンプル コードには、HttpHandler.vb ファイルにカスタム HttpHandler クラス (ZIPHandler) が含まれています。 ZipHandler は IhttpHandler インターフェイスを実装し、すべての .zip ファイルのリクエストを処理します。
サンプル コードをテストするには、IIS に新しい仮想ディレクトリを作成し、そこにソース ファイルをコピーする必要があります。このディレクトリに download.zip というファイルを作成します (IIS と ASP.NET は 2GB を超えるダウンロードを処理できないことに注意してください。そのため、ファイルがこの制限を超えないようにしてください)。 aspnet_isapi.dll を介して .zip 拡張子をマップするように IIS 仮想ディレクトリを構成します。
HttpHandler クラス: ZIPHandler が
ASP.NET で .zip 拡張子をマップした後、クライアントがサーバーから .zip ファイルを要求するたびに、IIS は ZipHandler クラスの ProcessRequest メソッドを呼び出します (ダウンロード コードを参照)。
ProcessRequest メソッドは、まずカスタム FileInformation クラス (ダウンロード コードを参照) のインスタンスを作成します。このインスタンスは、ダウンロードのステータス (進行中、中断など) をカプセル化します。この例では、download.zip サンプル ファイルへのパスがコードにハードコードされています。このコードを独自のアプリケーションに適用する場合は、要求されたファイルを開くようにコードを変更する必要があります。
' objRequest を使用してどのファイルが要求されたかを検出し、そのファイルを使用して objFile を開きます。
' 例: objFile = New Download.FileInformation(<完全なファイル名>)
objFile = 新しいダウンロード.ファイル情報( _
objContext.Server.MapPath("~/download.zip"))
次に、プログラムは、記述された HTTP ヘッダーを使用してリクエストを実行します (ヘッダーがリクエストに指定されている場合)。私は太陽の下で初めて祈りました。検証チェックが失敗した場合、応答は直ちに終了され、適切な StatusCode 値が送信されます。
objRequest.HttpMethod.Equals(HTTP_METHOD_GET) でない場合
objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) 次に
' 現在サポートされているのは GET メソッドと HEAD メソッドのみです objResponse.StatusCode = 501 ' 実行されません
ElseIf Not objFile.Exists then
' 要求されたファイルが見つかりませんでした objResponse.StatusCode = 404 ' 見つかりません
ElseIf objFile.Length > Int32.MaxValue then
'ファイルが大きすぎます objResponse.StatusCode = 413 'リクエストエンティティが大きすぎます
ElseIf Not ParseRequestHeaderRange(objRequest, alRequestedRangesBegin, alRequestedRangesend, _
objFile.Length、bIsRangeRequest) 次に、
' Range リクエストには無駄なエンティティが含まれています objResponse.StatusCode = 400 ' 無駄なリクエスト
ElseIf Not CheckIfModifiedSince(objRequest,objFile) then
'エンティティは変更されていません objResponse.StatusCode = 304 'エンティティは変更されていません
ElseIf Not CheckIfUnmodifiedSince(objRequest,objFile) then
' エンティティは最後に要求された日以降に変更されています objResponse.StatusCode = 412 ' 前処理に失敗しました
ElseIf Not CheckIfMatch(objRequest, objFile) then
' エンティティがリクエストと一致しません objResponse.StatusCode = 412 ' 前処理に失敗しました
ElseIf Not CheckIfNoneMatch(objRequest, objResponse,objFile) then
' エンティティは不一致リクエストに一致します。
'応答コードは CheckIfNoneMatch 関数内にあります
それ以外
'事前チェックに成功しました
これらの予備チェックの ParseRequestHeaderRange 関数 (ダウンロード コードを参照) は、クライアントがファイル範囲 (部分的なダウンロードを意味する) を要求したかどうかをチェックします。要求された範囲が無効な場合 (無効な範囲とは、ファイル サイズを超える範囲値、または不当な数値が含まれる場合)、このメソッドは bIsRangeRequest を True に設定します。範囲が要求された場合、CheckIfRange メソッドは IfRange ヘッダー情報を検証します。
要求された範囲が有効な場合、コードは応答メッセージのサイズを計算します。クライアントが複数の範囲を要求した場合、応答サイズ値にはマルチパート ヘッダーの長さの値が含まれます。
送信されたヘッダー値を特定できない場合、プログラムはダウンロード要求を部分ダウンロードではなく初期要求として処理し、ファイルの先頭から始まる新しいダウンロード ストリームを送信します。
bIsRangeRequest AndAlso CheckIfRange(objRequest, objFile) の場合
「これは範囲リクエストです」 Range 配列に複数のエンティティが含まれている場合、それはマルチパート範囲リクエストでもあります bMultipart = CBool(alRequestedRangesBegin.GetUpperBound(0)>0)
' 各範囲に移動して応答全体の長さを取得します For iLoop = alRequestedRangesBegin.GetLowerBound(0) To alRequestedRangesBegin.GetUpperBound(0)
'コンテンツの長さ (この範囲内)
iResponseContentLength += Convert.ToInt32(alRequestedRangesend( _
iLoop) - alRequestedRangesBegin(iLoop)) + 1
bマルチパートの場合
' マルチパート範囲リクエストの場合、送信する中間ヘッダ情報の長さを計算する iResponseContentLength += MULTIPART_BOUNDARY.Length
iResponseContentLength += objFile.ContentType.Length
iResponseContentLength += alRequestedRangesBegin(iLoop).ToString.Length
iResponseContentLength += alRequestedRangesend(iLoop).ToString.Length
iResponseContentLength += objFile.Length.ToString.Length
' 49 は、マルチパート ダウンロードでの改行およびその他の必要な文字の長さです。 iResponseContentLength += 49
終了の場合
Next iLoop
If bMultipart then
' マルチパート範囲リクエストの場合、
' 送信される最後の中間ヘッダーの長さも計算する必要があります iResponseContentLength +=MULTIPART_BOUNDARY.Length
' 8 はダッシュと改行の長さです iResponseContentLength += 8
それ以外
' マルチパート ダウンロードではないため、最初の HTTP ヘッダーの応答範囲を指定する必要があります objResponse.AppendHeader( HTTP_HEADER_CONTENT_RANGE, "bytes " & _
alRequestedRangesBegin(0).ToString & "-" & _
alRequestedRangesend(0).ToString & "/" & _
objFile.Length.ToString)
'終了の場合
' 範囲応答 objResponse.StatusCode = 206 ' 部分応答 Else
' これはスコープ リクエストではないか、要求されたスコープ エンティティ ID が現在のエンティティ ID と一致しません。
「新しいダウンロードを開始します」は、ファイルの完了した部分のサイズがコンテンツの長さに等しいことを示します。 iResponseContentLength =Convert.ToInt32(objFile.Length)
'通常のOKステータスに戻る objResponse.StatusCode = 200
終了の場合
' 次に、サーバーは、コンテンツの長さ、Etag、ファイル コンテンツ タイプなど、いくつかの重要な応答ヘッダーを送信する必要があります。
' コンテンツの長さをレスポンスに書き込みます objResponse.AppendHeader( HTTP_HEADER_CONTENT_LENGTH,iResponseContentLength.ToString)
' 最終変更日をレスポンスに書き込みます objResponse.AppendHeader( HTTP_HEADER_LAST_MODIFIED,objFile.LastWriteTimeUTC.ToString("r"))
' 範囲リクエストを受け入れたことをクライアント ソフトウェアに伝えます objResponse.AppendHeader( HTTP_HEADER_ACCEPT_RANGES,HTTP_HEADER_ACCEPT_RANGES_BYTES)
' ファイルのエンティティ タグを応答に書き込みます (引用符で囲みます)
objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, """" & objFile.EntityTag & """")
'コンテンツタイプをresponseIf bMultipart thenに書き込みます
「マルチパート メッセージにはこの特別なタイプがあります」 この例では、ファイルの実際の MIME タイプは後で応答に書き込まれます。 objResponse.ContentType = MULTIPART_CONTENTTYPE
それ以外
'単一の部分メッセージが所有するファイル コンテンツ タイプ objResponse.ContentType = objFile.ContentType
終了の場合
ダウンロードに必要なものがすべて準備できたので、ファイルのダウンロードを開始できます。 FileStream オブジェクトを使用して、ファイルからバイトのチャンクを読み取ります。 FileInformation インスタンス objFile の State プロパティを fsDownloadInProgress に設定します。クライアントが接続されている限り、サーバーはファイルからバイトのチャンクを読み取り、クライアントに送信します。マルチパート ダウンロードの場合、このコードは特定のヘッダー情報を送信します。クライアントが切断されると、サーバーはファイルのステータスを fsDownloadBroken に設定します。サーバーが要求された範囲の送信を完了すると、ステータスを fsDownloadFinished に設定します (ダウンロード コードを参照)。
FileInformation 補助クラス
ZIPHandler セクションでは、FileInformation がダウンロード ステータス情報 (ダウンロード、中断など) をカプセル化する補助クラスであることがわかります。
FileInformation のインスタンスを作成するには、要求されたファイルへのパスをクラスのコンストラクターに渡す必要があります:
Public Sub New(ByVal sPath As String)
m_objFile = 新しい System.IO.FileInfo(sPath)
End Sub
FileInformation は、System.IO.FileInfo オブジェクトを使用して、オブジェクトのプロパティとして公開されるファイル情報 (ファイルが存在するかどうか、ファイルの完全名、サイズなど) を取得します。
このクラスは
、ダウンロード リクエストのさまざまな状態を記述する DownloadState 列挙も公開します。
' Clear: ダウンロード プロセスはありません。ファイルは fsClear = 1 を維持している可能性があります
'ロック済み: 動的に作成されたファイルは変更できません fsLocked = 2
'進行中: ファイルはロックされており、ダウンロード プロセスが進行中です fsDownloadInProgress = 6
'Broken: ファイルはロックされており、ダウンロード プロセスは進行中ですが、キャンセルされました fsDownloadBroken = 10
' 完了: ファイルはロックされ、ダウンロード プロセスが完了しました fsDownloadFinished = 18
End Enum
FileInformation は、EntityTag 属性値も提供します。サンプル コードでは 1 つのダウンロード ファイルのみが使用され、そのファイルは変更されないため、この値はコード例でハードコーディングされています。ただし、実際のアプリケーションでは、動的であっても複数のファイルを提供することになります。ファイルを作成するには、コードでファイルごとに一意の EntityTag 値。さらに、この値は、ファイルが変更または変更されるたびに変更する必要があります。これにより、クライアント ソフトウェアは、ダウンロードしたバイトのチャンクがまだ最新であることを確認できます。以下は、ハードコーディングされた EntityTag 値を返すサンプル コードの一部です。
Public ReadOnly Property EntityTag() As String
' EntityTag は、クライアントへの最初の (200) 応答と、クライアント Get からの回復リクエストに使用されます。
' ファイルに一意の文字列を作成します。
' ファイルが変更されない限り、一意のコードを保持する必要があることに注意してください。
' ただし、ファイルが変更された場合、または変更された場合は、このコードも変更する必要があります。
「MyExampleFileID」を返します
終了取得
End プロパティ
シンプルで一般に十分安全な EntityTag は、ファイル名とファイルが最後に変更された日付で構成されます。どのような方法を使用する場合でも、この値が真に一意であり、他のファイルの EntityTag と混同できないことを確認する必要があります。アプリケーション内で作成されたファイルにクライアント、顧客、郵便番号インデックスに基づいて動的に名前を付け、EntityTag として使用される GUID をデータベースに保存したいと考えています。
ZipFileHandler クラスは、パブリック State プロパティを読み取り、設定します。ダウンロードが完了すると、State が fsDownloadFinished に設定されます。この時点で、一時ファイルを削除できます。ここでは通常、状態を維持するために Save メソッドを呼び出す必要があります。
Public プロパティ State() As DownloadState
得る
m_nState を返す
終了取得
Set(ByVal nState As DownloadState)
m_nState = nState
' オプションのアクション: この時点でファイルを自動的に削除できます。
' ステータスが完了に設定されている場合、このファイルは必要ありません。
' nState =DownloadState.fsDownloadFinished の場合、次に
'クリア()
'それ以外
'保存()
'終了の場合
保存()
エンドセット
End プロパティ
ZipFileHandler は、ファイルのステータスが変更されるたびに Save メソッドを呼び出してファイルのステータスを保存し、後でユーザーに表示できるようにする必要があります。自分で作成した EntityTag を保存するために使用することもできます。ファイルの状態と EntityTag 値をアプリケーション、セッション、またはキャッシュに保存しないでください。これらすべてのオブジェクトのライフサイクル全体にわたって情報を保存する必要があります。
PrivateSubSave()
' ファイルのダウンロード ステータスをデータベースまたは XML ファイルに保存します。
' もちろん、ファイルを動的に作成しない場合は、この状態を保存する必要はありません。
で
は既存のファイル (download.zip) のみを処理しますが、このプログラムをさらに拡張して、必要に応じて要求されたファイルを作成することができます。
サンプル コードをテストするときは、ローカル システムまたは LAN が速すぎてダウンロード プロセスを中断できない可能性があるため、低速の LAN 接続を使用するか (IIS でサイトの帯域幅を減らすのがシミュレーション方法です)、サーバーを次の場所に配置することをお勧めします。インターネット。
クライアントでのファイルのダウンロードは依然として困難です。 ISP が運用する Web キャッシュ サーバーが正しくない、または構成が間違っていると、ダウンロード パフォーマンスの低下やセッションの早期終了など、大きなファイルのダウンロードが失敗する可能性があります。ファイル サイズが 255 MB を超える場合は、サードパーティのダウンロード管理ソフトウェアを使用するよう顧客に勧める必要があります。ただし、最近の一部のブラウザには基本的なダウンロード マネージャーが組み込まれています。
サンプル コードをさらに拡張したい場合は、HTTP 仕様を参照すると役立つ場合があります。ダウンロード用の MD5 チェックサムを確立し、Content-MD5 ヘッダーを使用してチェックサムを追加すると、ダウンロードされたファイルの整合性を検証する方法が提供されます。サンプル コードには、GET と HEAD 以外の他の HTTP メソッドは含まれていません。