Silverlight + nginx =再開可能なブラウザファイルのダウンロード

この記事では、 Files @ mail.ruプロジェクトで更新可能なファイルのダウンロードを組織するためにSilverlightクライアントを実装した経験について説明します。



なぜこれが必要なのですか? サーバーにファイルをアップロードして保存することで、小規模から大規模まで、非常に多くのWebプロジェクトが提供されると言う必要はないと思います。 さらに、ダウンロードは通常、通常の<input type=file/>



の形式で実装されますが、Flashを使用することは少なく、他の方法を使用することも少なくなります(この記事ではFTPダウンロードを考慮しません)。



問題は、HTTPプロトコルが最初はテキスト形式であり、大量のバイナリデータの転送にはあまり適していないことです。 そのため、ユーザーがコンピューターを切断したり、コンピューターを再起動したりすると、半分転送されたファイルを再度ダウンロードする必要があります。また、低速チャンネルの場合、これは真のm笑になります。



どうする



どうしてそんな人生にたどり着いたのか



ファイルの内容を読み取るためのFileReference.load()メソッドをサポートするAdobe Flash 10の新しいバージョンに関する最初の噂は、私たちにインスピレーションを与えました。 しかし、アドビは「誰よりも優れていました」。 FileReference.load()メソッドは、ファイルの内容全体をコンピューターのメモリに完全にダウンロードし、それにより大きなファイルを読み取ろうとするとマシンを「サスペンド」します(実験では、ファイルはすでに2 GBのRAMを搭載したコンピューターで約500 MBでした)。 また、Flashは2GBを超えるファイルをサポートしていません。



悲しくてがっかりしました。 さらに、サーバー側からの部分ロードのサポートが緊急に必要であり、それを自分で行うのは面倒でした。



そして、「Silverlightを見てみましょう。Flash以外のものが得られるかもしれませんか?」と誤解されませんでした。



Silverlightでは、ファイルの操作がFlashよりも適切に実装され、アクセス可能です。ダイアログでユーザーが選択したファイルを、任意のサイズのバッファーによる任意のオフセットで読み取ることができます。 同時に、Silverlightのファイルサイズは64ビットの数値によって制限されます。 ほぼ無限のサイズ(理論的には最大16 384 PB)のファイルをアップロードできます。



さらに Valery Kholodkov リポジトリ (誰かが突然知らない場合、これはファイルをダウンロードするため優れたnginx_upload_moduleモジュールの作成者です)、partial-uploadと呼ばれるブランチが出現し、ゆっくりと開発されました。



Valeryのサポートにより、Silverlightクライアントの記述を開始し、サーバーモジュールと「ドッキング」しました...



ハッピーエンド



クライアントコードを何時間も書き直してテストした後、ようやく最初の有効なオプションが手に入りました。



息を切らして、最初のファイルのダウンロードを開始しました。ダウンロードプロセス中にネットワークケーブルを引き抜いた後、元の場所に戻してみると、なんと幸運なことに、ダウンロードはほとんど崖の先から再開されます。 しかし、至福はすぐに過ぎ去った。 クイックテスト中に、クライアントコードとサーバーモジュールコードの両方にバグが見つかりました。



彼のモジュールのバグを迅速に修正してくれたValeryと、SilverlightとC#との戦いに感謝します。



8月のある晴れた日、ついに見つけたすべてのバグをテストして修正し、 Mail.Ru Filesのユーザーを幸せにするためにこれを活用することに失敗しませんでした。



そして最後に-スタジオでの決定!



クライアントとサーバーの相互作用について少し



ダウンロードは次のとおりです。



クライアントは、アップロードされたファイルごとに一意のセッション識別子を生成します。

SessionId = (1100000000 + new Random ().Next(10000000, 99999999)).ToString();



* This source code was highlighted with Source Code Highlighter .






また、各ファイルに対してハッシュが考慮されます。その目的は、ユーザーのコンピューター内で一意のファイルを一意に識別することです。

UniqueKey = "" ;

try

{

if (FileLength < Constants.MinFilesizeToAdd)

{

throw new Exception();

}

// Adler32 version to compute "unique" file hash

// UniqueKey will be Constants.NumPoints * sizeof(uint) length

int part_size = ( int )((file.Length / Constants.NumPoints) < Constants.MaxPartSize ? file.Length / Constants.NumPoints : Constants.MaxPartSize);

byte [] buffer = new Byte [part_size];

byte [] adler_sum = new Byte [Constants.NumPoints * sizeof ( uint ) / sizeof ( byte )];

int current_point = 0;

int bytesRead = 0;

Stream fs = file.OpenRead();

AdlerChecksum a32 = new AdlerChecksum();

while (current_point < Constants.NumPoints && (bytesRead = fs.Read(buffer, 0, part_size)) != 0)

{

a32.MakeForBuff(buffer, bytesRead);

int mask = 0xFF;

for ( int i = 0; i < sizeof ( uint ) / sizeof ( byte ); i++)

{

UniqueKey += ( char )((mask << (i * sizeof ( byte )) & a32.ChecksumValue) >> (i * sizeof ( byte )));

}

fs.Position = ++current_point * file.Length / Constants.NumPoints;

}

}

catch (Exception) { }




* This source code was highlighted with Source Code Highlighter .






ダイアログでファイルを選択し、そのハッシュを計算した後、ローカルのSilverlightストレージでこのファイルに関する情報の可用性を確認し、情報がある場合は、ロードされたバイト範囲の最初の「ホール」からダウンロードを開始します。



次に、クライアントはファイルの一部を送信し、X-Content-Rangeヘッダーで送信されたバイトの範囲を示します(Silverlightの制限により、サーバーモジュールは両方のヘッダーをサポートしますが、このヘッダーはHTTPの標準Content-Rangeヘッダーの代わりに使用されます)およびSession-IDヘッダーのセッション識別子。 この場合、純粋なバイナリデータがリクエスト本文で送信されます。 作品の内容。

UriBuilder ub = new UriBuilder(UploadUrl);

HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(ub. Uri );

webrequest.Method = "POST" ;

webrequest.ContentType = "application/octet-stream" ;

// Some russian letters in filename lead to exception, so we do uri encode on client side

// and uri decode on server side

webrequest.Headers[ "Content-Disposition" ] = "attachment; filename=\"" + HttpUtility.UrlEncode( File .Name) + "\"" ;

webrequest.Headers[ "X-Content-Range" ] = "bytes " + currentChunkStartPos + "-" + currentChunkEndPos + "/" + FileLength;

webrequest.Headers[ "Session-ID" ] = SessionId;

webrequest.BeginGetRequestStream( new AsyncCallback(WriteCallback), webrequest);




* This source code was highlighted with Source Code Highlighter .






サーバーからのRange応答のヘッダーには、既にサーバーにアップロードされているこのファイルのバイト範囲のリストが含まれています。 このリストは、回答の本文にも複製されます(複製されたものについては、以下を参照)。



各チャンクが正常にロードされると、ロードされた範囲に関する情報がSilverlightローカルストレージに保存/更新されます。キーはファイルのハッシュです。 これにより、ブラウザを閉じた後でもファイルをリロードできます。 各チャンクをロードした後、サーバーモジュールはhttp-code 201を返しますが、ダウンロードリクエストはバックエンドにプロキシされません。



ファイルが完全にダウンロードされたとモジュールが判断すると、一時ファイルへのリンクを使用してバックエンドリクエストをプロキシします(標準のアップロードモジュールと同じ)。 実際、バックエンドでは、標準アップロードモジュールの使用から部分アップロードモジュールの使用への移行は完全に透過的です。 バックエンドコードを変更する必要はまったくありません。



回避する必要があったSilverlightの制限:



1. Content-Rangeヘッダーを設定できないため、X-Content-Rangeヘッダーを使用します



2.サーバーの応答コードを確実に判断することは不可能です。200または404のコードしか表示されません(SilverlightでブラウザーHTTPスタックを使用する場合)



3. SilverlightでクライアントHTTPスタックを使用すると、プロキシ認証が失われ、Cookieを手動で設定する必要がありますが、サーバー応答コードを正確に決定できます。したがって、HTTPブラウザースタックを使用して201応答コードを決定します。

if (ResponseText != null && ResponseText.Length != 0)

{

// We cannot check response.StatusCode, see comments in constructor of FileUploadControl

if (Regex.IsMatch(ResponseText, @"^\d+-\d+/\d+" )) // we got 201 response

{

...

}

else // we got 200 response

{

BytesUploaded = FileLength;

}

}




* This source code was highlighted with Source Code Highlighter .






4.大きなファイルの「正しい」ファイルハッシュ(md5など)の計算には時間がかかります-数十秒-これは許容できないため、100 KBのファイルの50の部分を取ります。各部分について、 Adler32アルゴリズムを使用して合計を計算します(このアルゴリズムは使い慣れたハッカーのアドバイスに基づいた高速な操作のため)、個々の金額を連結します-これは「一意の」ファイルハッシュです



5.ファイル名に特定のロシア語の文字が含まれるSilverlight(「z」という文字はMicrosoftに不利になりました)は、次の行で例外をスローしました...

webrequest.Headers[ "Content-Disposition" ] = "attachment; filename=\"" + File .Name + "\"" ;



* This source code was highlighted with Source Code Highlighter .






...そのため、変更が必要でした-起動時にファイル名をエンコードし、サーバーでデコードします

webrequest.Headers[ "Content-Disposition" ] = "attachment; filename=\"" + HttpUtility.UrlEncode( File .Name) + "\"" ;



* This source code was highlighted with Source Code Highlighter .






6.一定のバイト数を読み込んだ後にバッファーがリセットされても、SilverlightはPOST要求をキャッシュして完全に送信します。 これにより、ファイル全体を(チャンクなしで)ダウンロードすることができなくなります。 大きなファイルでは、クライアントのメモリはリクエストをバッファリングするのに十分ではありません。 この機能により、ダウンロードの進行状況を適切に表示することもできなくなります。



したがって、ファイルを100チャンクに分割して0%から100%の進行状況を表示しようとしていますが、同時に、非常に大きいファイルと非常に小さいファイルの場合はそれぞれ、チャンクのサイズを上下に制限します。はるかに少ない。

public long FileLength

{

get { return fileLength; }

set

{

fileLength = value ;

ChunkSize = ( long )(fileLength / (100 / Constants.PercentPrecision));

if (ChunkSize < Constants.MinChunkSize)

ChunkSize = Constants.MinChunkSize;

if (ChunkSize > Constants.MaxChunkSize)

ChunkSize = Constants.MaxChunkSize;

}

}




* This source code was highlighted with Source Code Highlighter .






7. Operaには不快なバグがあります(迷うことは既にありますが、どれか)。サーバーからの応答の長さが0の場合、Silverlightは応答リーダー読み取りハンドラーを呼び出しません。 これが、サーバー応答の本文でダウンロードされたバイトの範囲を複製するようValeryに依頼した理由です。



私たちは多くの不快なレーキを踏んだので、他の開発者の道をとげのないものにしたいのです。 そのため、クライアントパーツコードを開くことにしました。 MrUploaderに会ってください 。 Valery Kholodkovのnginx-uploadモジュールと合わせて特においしいです。



All Articles