JSON文字列のバイナリデータ。 Base64よりも優れたもの
質問
JSON形式はネイティブでバイナリデータをサポートしていません。バイナリデータは、JSONの文字列要素(バックスラッシュエスケープを使用した二重引用符で囲まれた0個以上のUnicode文字)に配置できるようにエスケープする必要があります。
バイナリデータをエスケープする明白な方法は、Base64を使用することです。ただし、Base64には高い処理オーバーヘッドがあります。また、3バイトを4文字に拡張するため、データサイズが約33%増加します。
この使用例の1つは、 CDMIクラウドストレージAPI仕様のv0.8ドラフトです。 。 JSONを使用して、REST Webサービスを介してデータオブジェクトを作成します。例:
PUT /MyContainer/BinaryObject HTTP/1.1
Host: cloud.example.com
Accept: application/vnd.org.snia.cdmi.dataobject+json
Content-Type: application/vnd.org.snia.cdmi.dataobject+json
X-CDMI-Specification-Version: 1.0
{
"mimetype" : "application/octet-stream",
"metadata" : [ ],
"value" : "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg
dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu
dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=",
}
バイナリデータをJSON文字列にエンコードするより良い方法と標準的な方法はありますか?
解決
JSON仕様に従って1バイトとして表現できるUnicode文字は94個あります(JSONがUTF-8として送信される場合)。それを念頭に置いて、空間的にできる最善の方法は、4バイトを表す base85 であると思います。 5文字として。ただし、これはbase64に比べて7%の改善に過ぎず、計算コストが高く、実装はbase64ほど一般的ではないため、おそらく勝てません。
すべての入力バイトをU + 0000-U + 00FFの対応する文字にマップし、JSON標準で必要な最小限のエンコードを行ってそれらの文字を渡すこともできます。ここでの利点は、必要なデコードが組み込み関数を超えてゼロであることですが、スペース効率が悪いことです-105%の拡張(すべての入力バイトが等しくなる可能性がある場合)対base85の25%またはbase64の33%。
最終判定:私の意見では、base64は、交換を保証するのは一般的で、簡単で、悪くないという理由で勝ちます。十分。
参照: Base91
他のヒント
同じ問題にぶつかり、解決策を共有すると思った: multipart / form-data。
マルチパートフォームを送信するには、まず JSONメタデータを文字列として送信し、次に Contentによってインデックス付けされた生のバイナリ(画像、wavなど)として個別に送信します-処分名前。
obj-cでこれを行う方法に関するチュートリアルまた、ブログ記事それは、フォーム境界で文字列データを分割し、バイナリデータから分離する方法を説明しています。
本当に必要な変更は、サーバー側のみです。 POSTされたバイナリデータを適切に参照するメタデータをキャプチャする必要があります(Content-Disposition境界を使用して)。
サーバー側で追加の作業が必要であることを認めましたが、多数の画像または大きな画像を送信する場合、これは価値があります。必要に応じて、これをgzip圧縮と組み合わせます。
base64でエンコードされたデータを送信するIMHOはハックです。 RFC multipart / form-dataは、次のような問題のために作成されました。バイナリデータをテキストまたはメタデータと組み合わせて送信します。
BSON(バイナリJSON)が機能する場合があります。 http://en.wikipedia.org/wiki/BSON
編集: 参考までに、.NETライブラリ json.net は、C#サーバー側の愛を探している場合、bsonの読み取りと書き込みをサポートしています。
UTF-8の問題は、それが最もスペース効率の良いエンコーディングではないことです。また、一部のランダムなバイナリバイトシーケンスは無効なUTF-8エンコーディングです。したがって、ランダムなバイナリバイトシーケンスをUTF-8データとして解釈することはできません。これは、UTF-8エンコードが無効になるためです。 UTF-8エンコーディングに対するこの制約の利点は、見始めるバイトを開始および終了するマルチバイト文字のロバスト性と位置特定を可能にすることです。
結果として、範囲[0..127]のバイト値のエンコードにUTF-8エンコードで1バイトのみが必要な場合、範囲[128..255]のバイト値のエンコードには2バイトが必要です。 それよりも悪い。 JSONでは、制御文字"および\は文字列に使用できません。したがって、バイナリデータを適切にエンコードするには、何らかの変換が必要になります。
見てみましょう。バイナリデータで均一に分散されたランダムバイト値を仮定すると、平均して、バイトの半分は1バイトでエンコードされ、残りの半分は2バイトでエンコードされます。 UTF-8でエンコードされたバイナリデータは、初期サイズの150%になります。
Base64エンコーディングは、初期サイズの133%までしか成長しません。そのため、Base64エンコードの方が効率的です。
別のベースエンコーディングの使用はどうですか? UTF-8では、128個のASCII値のエンコードが最もスペース効率が高くなります。 8ビットでは、7ビットを保存できます。したがって、バイナリデータを7ビットチャンクにカットして、UTF-8エンコード文字列の各バイトに保存すると、エンコードデータは初期サイズの114%にしか成長しません。 Base64よりも優れています。残念ながら、JSONでは一部のASCII文字が許可されないため、この簡単なトリックは使用できません。 ASCIIの33の制御文字([0..31]および127)および"および\は除外する必要があります。これにより、128-35 = 93文字しか残りません。
したがって、理論的には、エンコードされたサイズを8 / log2(93)= 8 * log10(2)/ log10(93)= 122%に増やすBase93エンコードを定義できます。ただし、Base93エンコードはBase64エンコードほど便利ではありません。 Base64では、入力ビットシーケンスを6ビットチャンクにカットする必要があります。この場合、単純なビット単位の操作がうまく機能します。 133%の横は122%を超えていません。
これが、Base64が実際にJSONでバイナリデータをエンコードするための最良の選択であるという共通の結論に私が独立して来た理由です。私の答えはそれを正当化するものです。パフォーマンスの観点からはあまり魅力的ではないことに同意しますが、JSONをすべてのプログラミング言語で操作しやすい人間が読み取れる文字列表現で使用することの利点も考慮してください。
パフォーマンスが重要な場合は、純粋なバイナリエンコーディングをJSONの代替と見なす必要があります。しかし、JSONでは、Base64が最適であるという結論になりました。
帯域幅の問題に対処する場合は、まずクライアント側でデータを圧縮してから、base64-itで圧縮してください。
このような魔法の良い例は http://jszip.stuartk.co.uk/ にあります。このトピックの詳細については、 GzipのJavaScript実装
をご覧ください。yEncはあなたのために働くかもしれません:
http://en.wikipedia.org/wiki/Yenc
" yEncは、バイナリを転送するためのバイナリからテキストへのエンコーディングスキームです。 [テキスト]のファイル。以前のUS-ASCIIベースのオーバーヘッドを削減します 8ビット拡張ASCIIエンコード方式を使用したエンコード方式。 yEncのオーバーヘッドは多くの場合(各バイト値がおよそ 平均して同じ頻度で)に比べて1〜2% uuencodeやBase64などの6ビットエンコード方式のオーバーヘッドは33%〜40%です。 ... 2003年までにyEncは、事実上の標準エンコーディングシステムになりました。 Usenetのバイナリファイル。"
ただし、yEncは8ビットエンコーディングであるため、JSON文字列に格納する場合、元のバイナリデータを格納するのと同じ問題があります。単純な方法で行うと、100%の拡張が必要になります。 / p>
エンコード、デコード、コンパクト化は非常に高速です
速度の比較(javaベースですが、それでも意味があります): https://github.com/eishay/jvm-serializers/ wiki /
また、バイト配列のbase64エンコードをスキップできるJSONの拡張機能です
スペースが重要な場合、スマイルエンコードされた文字列をgzipで圧縮できます
base64の拡張率が〜33%であることは事実ですが、処理オーバーヘッドがこれよりも大幅に大きいことは必ずしも真実ではありません。実際に使用しているJSONライブラリ/ツールキットに依存します。エンコードとデコードは単純な簡単な操作であり、文字エンコードで最適化することもできます(JSONはUTF-8 / 16/32のみをサポートするため)-base64文字はJSON文字列エントリでは常にシングルバイトです。 たとえば、Javaプラットフォームには、かなり効率的にジョブを実行できるライブラリがあるため、オーバーヘッドは主にサイズの拡張によるものです。
以前の2つの回答に同意します:
- base64はシンプルで一般的に使用される標準であるため、JSONで使用するのに特に適したものを見つけることはほとんどありません(base-85はpostscriptなどで使用されますが、考えてもメリットはほとんどありません)
- 使用するデータによっては、エンコード前(およびデコード後)の圧縮が非常に有効な場合があります
( 7年後に編集: Google Gearsは廃止されました。この回答は無視してください。)
Google Gearsチームは、バイナリデータタイプの不足の問題に遭遇し、それを解決しようとしました:
JavaScriptにはテキスト文字列用の組み込みデータ型がありますが、バイナリデータ用には何もありません。 Blobオブジェクトはこの制限に対処しようとします。
おそらくそれを何らかの方法で織り込むことができます。
バイナリデータを厳密にテキストベースの非常に限定された形式にシューホーンする機能を探しているので、Base64のオーバーヘッドは、JSONで維持することを期待している利便性に比べて最小限だと思います。処理能力とスループットが懸念される場合は、おそらくファイル形式を再検討する必要があります。
議論にリソースと複雑さの観点を追加するだけです。新しいリソースを保存して変更するためにPUT / POSTおよびPATCHを実行するため、コンテンツ転送は保存され、GET操作を発行することによって受信されるコンテンツの正確な表現であることを覚えておく必要があります。
マルチパートメッセージはしばしば救世主として使用されますが、単純さの理由とより複雑なタスクのために、コンテンツ全体を提供するというアイデアを好みます。自明であり、簡単です。
そして、はいJSONは不自由なものですが、最終的にJSON自体は冗長です。また、BASE64へのマッピングのオーバーヘッドは小さくする方法です。
マルチパートメッセージを正しく使用するには、送信するオブジェクトを分解するか、プロパティパスをパラメーター名として自動組み合わせに使用するか、ペイロードを単に表現するために別のプロトコル/フォーマットを作成する必要があります。
BSONアプローチも好みですが、これは望みどおりに広く簡単にサポートされていません。
基本的に、ここで何かを見逃していますが、実際のバイナリ転送を行う必要性を本当に確認していない限り、base64は十分に確立されており、進むべき方法としてバイナリデータを埋め込みます(ほとんどの場合そうではありません)。
データ型は本当に重要です。 RESTfulリソースからペイロードを送信するさまざまなシナリオをテストしました。エンコードにはBase64(Apache)を使用し、圧縮にはGZIP(java.utils.zip。*)を使用しました。ペイロードには、フィルム、画像、音声ファイルに関する情報が含まれています。画像と音声ファイルを圧縮およびエンコードしたため、パフォーマンスが大幅に低下しました。圧縮前のエンコードはうまくいきました。画像および音声コンテンツは、エンコードおよび圧縮されたバイトとして送信されました[]。
参照: http:// snia。 org / sites / default / files / Multi-part%20MIME%20Extension%20v1.0g.pdf
バイナリデータのbase64変換を必要とせずに、「CDMIコンテンツタイプ」操作を使用してCDMIクライアントとサーバー間でバイナリデータを転送する方法について説明します。
「非CDMIコンテンツタイプ」操作を使用できる場合、オブジェクトとの間で「データ」を転送することが理想的です。その後、後続の「CDMIコンテンツタイプ」操作としてメタデータをオブジェクトに追加したり、オブジェクトから取得したりできます。
Nodeを使用している場合、最も効率的で簡単な方法は次のようにUTF16に変換することだと思います:
Buffer.from(data).toString('utf16le');
次の方法でデータを取得できます。
Buffer.from(s, 'utf16le');
もう少し掘り下げて( base128 の実装中に)、文字を送信するときにそのを公開しますどのASCIIコードが128よりも大きい場合、実際にはブラウザ(クロム)は1つではなく2つの文字(バイト)を送信します(。理由は、defaulによるJSONはutf8文字を使用するためです。 chmike の回答で言及された2バイトの答え:この方法でテストを行いました:chrome url barと入力します chrome: // net-export / を選択し、「生のバイトを含める」を選択し、キャプチャを開始し、POSTリクエストを送信し(下部のスニペットを使用)、キャプチャを停止して生のリクエストデータを含むjsonファイルを保存します。ファイル:
- base64リクエストは、
4142434445464748494a4b4c4d4e
という文字列を見つけることで見つけることができます。これはABCDEFGHIJKLMN
の16進コードであり、" byte_count&quot ;: 639
それのために。 - 上記の127リクエストは、文字列
C2BCC2BDC380C381C382C383C384C385C386C387C388C389C38AC38B
を見つけることで見つけることができます c1c2c3c4c5c6c7c8c9cacbcccdce )。" byte_count&quot ;: 703
で、base64リクエストより64バイト長い
したがって、実際には、コード> 127 :(を使用して文字を送信しても利益はありません。この問題は、 lex answer で説明されているPOST multipart / form-dataのバイナリ部分でデータを送信します(ただし、この場合は通常基本コーディングを使用する必要はありません...)。
別のアプローチでは、 base65280 / base65k のようなコードを使用して2バイトのデータ部分を1つの有効なutf8文字にマッピングすることに依存しますが、 utf8仕様 ...
function postBase64() {
let formData = new FormData();
let req = new XMLHttpRequest();
formData.append("base64ch", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
req.open("POST", '/testBase64ch');
req.send(formData);
}
function postAbove127() {
let formData = new FormData();
let req = new XMLHttpRequest();
formData.append("above127", "¼½ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüý");
req.open("POST", '/testAbove127');
req.send(formData);
}
<button onclick=postBase64()>POST base64 chars</button>
<button onclick=postAbove127()>POST chars with codes>127</button>
私の解決策は、XHR2がArrayBufferを使用していることです。バイナリシーケンスとしてのArrayBufferには、複数のコンテンツタイプを持つマルチパートコンテンツ、ビデオ、オーディオ、グラフィック、テキストなどが含まれます。すべてを1つの応答で。
さまざまなコンポーネントのDataView、StringView、およびBlobを備えた最新のブラウザー。 詳細については、 http://rolfrost.de/video.html もご覧ください。