ブラウザーからサーバーへの転送中のデータ圧縮

ブラウザで大量のデータを処理していますか?

それらをサーバーに送り返したいですか?

はい、それはより速く送信され、1つのhttpリクエストに入れられましたか?



この記事では、圧縮と最新のJavaScript機能を使用して、新しいプロジェクトでこの問題をどのように解決したかを示します。







タスクの説明



Habrauser anetoは、Yandex.Directがキーワード同士の交差をうまく処理していないと私に不平を言った。 一方、タスクは関連性があり、実際には手動で解決できません。 そこで、この問題を解決する小さなサービスを行いました。



多くの処理されたキーワードがあります-数万行。 2次の複雑さのため、処理アルゴリズムはメモリと計算能力を要求します。 したがって、ユーザーのブラウザーを引き付け、サーバーからクライアントに処理を移すことは罪ではありません。



開発中に、2つの問題がありました。

  1. ゆっくり接続すると、データの転送時間が長すぎます。
  2. 多くの場合、nginx / apache / php /などの制限により、データは単一の投稿リクエストに収まりません。




解決策



多くの解決策があります。 このケースでは、 Typed ArraysWorkersXHR 2という最新の標準に基づいたバージョンをリリースしました。 簡単に言うと、データを圧縮してバイナリ形式でサーバーに送信します。 これらの単純なアクションにより、送信されるデータのサイズを2倍以上減らすことができました。



ステップごとにアルゴリズムを考えてみましょう。



ステップ0:ソースデータ


たとえば、多くのユーザーに関するさまざまなデータを含む配列を生成しました。 この例では、JSONPを介してロードされ、サーバーに送り返されます。



コードおよびデータ送信機能のダウンロード
<script> function setDemoData(data) { window.initialData = data; } function send(data) { var http = new XMLHttpRequest(); http.open('POST', window.location.href, true); http.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); http.onreadystatechange = function() { if (http.readyState == 4) { if (http.status === 200) { // xhr success } else { // xhr error; } } }; http.send(data); } </script> <script src="http://nodge.ru/habr/demoData.js"></script>
      
      





データをそのまま送信して、デバッガーを見てみましょう。



 var data = JSON.stringify(initialData); send(data);
      
      









単純な転送では、要求ボリュームは9402 Kbです。 たくさん、カットします。



ステップ1:データ圧縮


JavaScriptのデータ圧縮用の組み込み関数はありません。 圧縮には、 LZWDeflateLZMAなど、便利な任意のアルゴリズムを使用できます。 選択は、主にクライアントとサーバーのライブラリの可用性に依存します。 対応するjavascriptライブラリは、githubに簡単に配置できます: onetwothree



3つすべてのオプションを使用しようとしましたが、PHPを使用してLZWだけを友達にできました。 これは非常に単純なアルゴリズムです。 例では、この実装を使用します。



LZW圧縮機能
 var LZW = { compress: function(uncompressed) { "use strict"; var i, l, dictionary = {}, w = '', k, wk, result = [], dictSize = 256; // initial dictionary for (i = 0; i < dictSize; i++) { dictionary[String.fromCharCode(i)] = i; } for (i = 0, l = uncompressed.length; i < l; i++) { k = uncompressed.charAt(i); wk = w + k; if (dictionary.hasOwnProperty(wk)) { w = wk; } else { result.push(dictionary[w]); dictionary[wk] = dictSize++; w = k; } } if (w !== '') { result.push(dictionary[w]); } result.dictionarySize = dictSize; return result; } };
      
      





LZWはASCIIで動作するように設計されているため、事前にUnicode文字をエスケープします。 ここに図書館があります

そのため、データを圧縮してサーバーに送信します。



 var data = JSON.stringify(initialData); data = stringEscape(data); data = LZW.compress(data); send(data.join('|'));
      
      





要求ボリューム-6079 Kb(圧縮65%)、3323 Kbを節約。 より洗練された圧縮アルゴリズムはより良い結果を示しますが、次のステップに進みます。



ステップ2:バイナリデータに変換する


LZWによる圧縮後、数値の配列を取得するため、文字列として渡すことは完全に非効率的です。 バイナリデータとして渡す方がはるかに効率的です。

このために、 型付き配列を使用できます。



 //  16-  32-       var type = data.dictionarySize > 65535 ? 'Uint32Array' : 'Uint16Array', count = data.length, buffer = new ArrayBuffer((count+2) * window[type].BYTES_PER_ELEMENT), //        bufferBase = new Uint8Array(buffer, 0, 1), //          LZW bufferDictSize = new window[type](buffer, window[type].BYTES_PER_ELEMENT, 1), bufferData = new window[type](buffer, window[type].BYTES_PER_ELEMENT*2, count); bufferBase[0] = type === 'Uint32Array' ? 32 : 16; //    bufferDictSize[0] = data.dictionarySize; //    LZW bufferData.set(data); //   data = new Blob([buffer]); //  ArrayBuffer  Blob    XHR send(data);
      
      





リクエスト量-3686 Kb(圧縮39%)、6079 Kbの節約。 これで、リクエストサイズが半分以上になり、説明した問題の両方が解決されました。



ステップ3:サーバーでの処理。


サーバーに到着したデータは、処理する前に解凍する必要があります。 当然、クライアントと同じアルゴリズムを使用する必要があります。 phpでこれを行う方法の例を次に示します。



PHP処理の例
 <?php $data = readBinaryData(file_get_contents('php://input')); $data = lzw_decompress($data); $data = unicode_decode($data); $data = json_decode($data, true); function readBinaryData($buffer) { $bufferType = unpack('C', $buffer); //   -   if ($bufferType[1] === 16) { $dataSize = 2; $unpackModifier = 'v'; } else { $dataSize = 4; $unpackModifier = 'V'; } $buffer = substr($buffer, $dataSize); // remove type from buffer $data = new SplFixedArray(strlen($buffer)/$dataSize); $stepCount = 2500; //    2500  for ($i=0, $l=$data->getSize(); $i<$l; $i+=$stepCount) { if ($i + $stepCount < $l) { $bytesCount = $stepCount * $dataSize; $currentBuffer = substr($buffer, 0, $bytesCount); $buffer = substr($buffer, $bytesCount); } else { $currentBuffer = $buffer; $buffer = ''; } $dataPart = unpack($unpackModifier.'*', $currentBuffer); $p = $i; foreach ($dataPart as $item) { $data[$p] = $item; $p++; } } return $data; } function lzw_decompress($compressed) { $dictSize = 256; //   -   $dictionary = new SplFixedArray($compressed[0]); for ($i = 0; $i < $dictSize; $i++) { $dictionary[$i] = chr($i); } $i = 1; $w = chr($compressed[$i++]); $result = $w; for ($l = count($compressed); $i < $l; $i++) { $entry = ''; $k = $compressed[$i]; if (isset($dictionary[$k])) { $entry = $dictionary[$k]; } else { if ($k === $dictSize) { $entry = $w . $w[0]; } else { return null; } } $result .= $entry; $dictionary[$dictSize++] = $w .$entry[0]; $w = $entry; } return $result; } function replace_unicode_escape_sequence($match) { return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE'); } function unicode_decode($str) { return preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $str); }
      
      





他の言語については、すべてが同じくらい簡単だと思います。



ステップ4:労働者


上記のコードは膨大なデータを圧縮するため、ページは圧縮中にフリーズします。 かなり不快な効果。 それを取り除くには、すべての計算を実行するストリームを作成します。 javascriptにはこのためのWorkersがあります。 ワーカーの使用方法は、以下の完全な例またはドキュメントに記載されています。



ステップ5:ブラウザーサポート


明らかに、上記のJavaScriptコードはIE6では機能しません=)

作業には、 Typed ArraysXHR 2およびWorkersが必要です。

サポートされているブラウザのリスト:IE10 +、Firefox 21 +、Chrome 26 +、Safari 5.1 +、Opera 15 +、IOS 5 +、Android 4.0+(ワーカーなし)。



検証には、Modernizr、または次のコードのようなものを使用できます。

必要な標準のサポートを定義する
 var compressionSupported = (function() { var check = [ 'Worker', 'Uint16Array', 'Uint32Array', 'ArrayBuffer', // Typed Arrays 'Blob', 'FormData' // xhr2 ]; var supported = true; for (var i = 0, l = check.length; i<l; i++) { if (!(check[i] in window)) { supported = false; break; } } return supported; })();
      
      







この記事のコードは、JS Bin: pageworkerで公開されています 。 ページを開き、開発者ツールを開いて、3つの投稿リクエストのサイズを確認します。



実際のプロジェクトでは、ソリューションはここで機能します 。 テストファイルをダウンロードし、そのファイルに固有のものを追加してキャッシュをバイパスし、アップロードして処理することができます。



おわりに



もちろん、この方法はすべての場合に適しているわけではありませんが、生命に対する権利があります。 圧縮ではなく複数のリクエストを行う方が簡単/賢い場合があります。 または、最初は数値データがあり、それを文字列に変換して圧縮する必要はありません-型付き配列を使用するだけです。



要約:





質問に答えて、コードの改善を受け入れます。 エラーとタイプミスをチェックしましたが、念のため-プライベートメッセージを書いてください。 すべてに良い。



更新1:

それとは別に、画像について言う価値があります。 ほとんどの形式(jpeg、png、gif)はすでに圧縮されているため、再度圧縮しても意味がありません。 画像は、文字列形式(base64)ではなく、バイナリデータとして送信する必要があります。 base64からblobへの変換を示すキャンバスの小さなを作成しました。



更新2:

SSLを使用している、またはSSLを使用する予定がある場合は、 この記事をお読みください 。 SSLはすでにリクエストの双方向圧縮を提供しています。



更新3:

base64をUnicode文字のエスケープに置き換えました。 はるかに効率的でした。 消費者seriyPSTolTolに感謝します。



All Articles