C++ での Base 64 エンコーディングの実装
このチュートリアルでは、C++ の base_64 でのエンコーディングについて説明します。
最初に、base_64 エンコーディングと、それが必要な理由と場所について説明します。 後で、C++ での base_64 のエンコード/デコードについて説明します。
エンコード方式 Base_64
Base_64 はエンコーディング スキームへの追加です。 これは、バイナリ データを ASCII 文字列で表すという点で、バイナリからテキストへのエンコーディングに似ています。
違いは、base_64 エンコーディングが基数 64 への変換を使用することです。 Base_64 エンコーディング名は、塩基の数学的定義に由来します。
基数は、数体系の基数を表します。 同様に、base_2 には 0 と 1 の 2つの基本数字しかありません。
8 進数システムであるBase_8には、0 から 7 までの 8つの基本数字があります。
同様に、base_16 には 0 から 15 までの 16 個の基本数字があり、A から F を使用して 10 から 15 を表します。 base_64 には、以下で構成される 64 個の基本数字があります。
- 26 大文字のアルファベット
- 26個の小さなアルファベット
- 0~9の10桁
- 2つの記号
+と/
Base_64エンコーディングは、ASCII を処理するように設計されたメディアを介してデータを転送するために一般的に使用されます。 base_64は、メディアを介して送信されるデータの整合性を維持しようとします。
主なアプリケーションは、MIME 経由の電子メールであり、複雑なデータを XML で保存します。 Base_64 はプライバシー強化電子メール (PEM) とも呼ばれます。
Base_64 ステップのエンコード
エンコードの場合、データはバイナリ文字列としてあり、文字列内の各文字を操作する必要があります。 base_64 でエンコードするには、次の手順を実行する必要があります。
-
各文字の ASCII 値を取得します。
-
ASCII 値の 8 ビット バイナリを見つけます。
-
ステップ2で得た8ビットを、桁を並べ替えて6ビットに変換します(一部のビット操作を含む何らかの操作が必要です(後述))。
-
6 ビット バイナリを対応する 10 進数値に変換します。
-
base_64(すでに説明したbase_64の基本数字) を使用して、それぞれのbase_64文字を各 10 進数値に割り当てます。
ここでは、8 ビット グループから 6 ビット グループへの変換であるステップ 3 の詳細について説明します。
8 ビット グループを 6 ビット グループに変換する手順
最初に説明したように、Base_64 エンコーディングでは、64 個の主要な文字/数字がありますが、通常はデータをバイト単位で読み書きします。 1 バイトは 8 ビットで、0 から 255 を格納できます。つまり、1 バイトで一意の 256 を表すことができます。
6 ビットは 64 の一意の値を表すことができます。最後の 2 ビットは 0 のままにして、各バイトが Base_64 エンコード方式の 1 桁/文字のみを格納するようにする必要があります。
各文字/ASCII 値は 8 ビットを使用します。 したがって、各バイトの 2 ビットを調整するには、元のデータよりも多くのストレージが必要になります。
Base_64 エンコーディングでは、データを失うことなく 6 ビットに変換する必要があります。
8 と 6 の LCM を取ると、24 になります。3 バイトには 24 ビットがありますが、8 ビットのうち 6 ビットを使用すると (最後の 2 ビットは使用されません)、24 ビットには 4 バイトが必要です。 したがって、データを失うことなく、3つの 8 ビット グループのそれぞれを 4つの 6 ビット グループに変換できます。
最初のステップは、データを 3 バイトのセットにグループ化することです。 最後のグループのバイト数が少ない場合、値が 0 のバイトを追加してグループを完成させます。
次に、3 バイトの各セットは、次の操作を使用して 4 バイトにグループ化されます。 3 バイトのセットを t1, t2 & t3 として、4 バイトを f1, f2, f3 & f4 として考えてみましょう。
f1 = ( t1 & 0xfc ) >> 2
マスク 0xfc (バイナリ 11111100 に相当) を考慮し、ビット単位で適用し、セットの最初のバイトとマスクの間で操作します。 次に、ビット単位の演算結果に対して右シフトを 2 回使用します。
シフト操作は左の 6 ビットを右に転送し、最後の 2 ビットは 0 になります。
マスク 0xfc の最初の 2 ビットは 0 です。 演算によってセットの最初のバイトの最初の 2 ビットが 0 になる場合 (つまり、最初のバイトの最後の 6 ビットが考慮されることを意味します)、最初の 2 ビット (この演算では無視されます) は次のプロセスで考慮されます。
f2 = ( ( t1 & 0x03 ) << 4 ) + ( ( t2 & 0xf0 ) >> 4 )
ここでは、演算の最初のバイトにマスク 0x03 00000011 が適用されます (つまり、最初の 2 ビットのみが考慮され、最後の 6 ビットは前の演算で既に考慮されています)。 シフト操作は、最初のバイトの結果の 2 ビットを左に転送し、式の 5 番目と 6 番目のビットにします。
マスク 0xf0 11110000 は、演算の 2 番目のバイトに適用されます (つまり、最後の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを右に転送して、式の最初の 4 ビットにします。
式の最初の部分では 5 番目と 6 番目のビットがオンになり、2 番目の部分では最初の 4 ビットがオンになり、全体として、最初の 6 ビットがオンになり、最後のビットがオフになります。
最後に、それらを組み合わせて、最後の 2 ビットがオフになっているバイトを取得します。 このステップでは、6 ビットの別のバイトを取得しました。最初のバイトが完了し、2 番目のバイトの最初の 4 ビットが考慮されます。
f3 = ( ( t2 & 0x0f ) << 2 ) + ( ( t3 & 0xc0 ) >> 6 )
マスク 0x0f 00001111 は、操作のために 2 番目のバイトに適用されます (つまり、最初の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを左に転送して式の 3、4、5、6 ビットにし、最初の 2 ビット用のスペースを作成します。
次に、操作のためにマスク 0xc0 11000000 が 3 番目のバイトに適用されます (つまり、最初の 2 ビットのみが考慮されます)。 シフト操作は、結果の 2 ビットを右に転送して、式の最初と 2 番目のビットにします。
最後に、両方の結果を組み合わせて、6 ビット グループの 3 番目のバイトを取得します。 繰り返しますが、セットでは、セットの 2 番目のバイトと 3 番目のバイトの 2 ビットが完了しました。
f4 = t3 & 0x3f
最後に、3 番目のバイトには操作のみがあり、マスク 0x3f 00111111 は最初の 6 ビットがオンで、最後の 2 ビットがオフです。 3 番目のバイトの操作では、3 番目のバイトの残りの 6 ビットが考慮されます。
base_64 で使用される 64 個の基本数字については既に説明しました。 次のステップでは、4 バイトのセット (ビット演算を使用して取得) の各バイトが base_64 に変換され、文字列に連結されます。
PLAY という単語をエンコードしてみましょう。 最初のステップでは、それぞれ 3 キャラクターのセットを作成します。 最初のセットには、PLA があります。
次の段階では、Y\0\0 があります。 ここで、\0 は、セットを完成させるために追加されたヌル文字です。
これらの各文字の ASCII は 80 76 65 89 0 0 です。 対応するバイナリ値は 01010000 01001000 01000001 01011001 です。
では、ビット操作をしましょう。
f1 = 01010000 & 11111100 = 01010000 >> 2 = 00010100 = 2001010000 & 00000011 = 0000000 << 4 = 00000000式の最初の部分01001000 & 11110000 = 01010000 >> 4 = 00000101式の 2 番目の部分f2 = 00000000 + 00000101 = 00000101 = 5、最初の部分と 2 番目の部分の結果を加算01001000 & 00001111 = 00001000 << 2 = 00100000式の最初の部分01000001 & 11000000 = 01000000 >> 4 = 00000100式の 2 番目の部分f3 = 00100000 + 00000100 = 00100100 = 36、最初の部分と 2 番目の部分の結果を加算f4 = 01000001 & 00000011 = 00000001 = 1
次に、2 番目と 3 番目の値が 0 である次のセットで操作を繰り返します。 したがって、結果は次のようになります。
f1 = 00010110 = 21
f2 = 00010000 = 16
f3 = 0
f4 = 0
次に、これらの値を base_64 に変換する必要があります。 また、最後の 2 バイトにいくつかのセンチネル/特殊文字を配置して、デコード プロセスがそれらを認識し、それに応じてデコードできるようにする必要があります。
最初のセットでは、f1= 20, f2 = 5, f3 = 36 & f4 = 1. 対応する base_64 値は UFkB になります。
次のセットは f1 = 21, f2 = 16, f3 = 0 & f4 = 0. ここでも、対応する base_64 値は VQ^^ になり、キャレット記号が特殊文字として使用されるため、文字列全体は UFkBV^^ になります。
デコード プロセスは単純に逆のプロセスです。 以下の C++ コードから両方のメソッドをすばやく取得できます。
C++ でのBase_64エンコーディングの実装
C++ でエンコードを行うのは簡単なプロセスです。 C++ で (説明した手順を) すぐに実装できます。
段階的に説明し、最後に 2つの例を含む完全なコードを示します。
まず、base_64 変換のために、base_64 の基本的な数字/文字を含む定数文字列を定義します。
const string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
関数のコーディングとデコードについて説明する前に、最初にいくつかの定義があります。 主に、エンコードとデコードに使用されるマスクがいくつかあります。
これらのマスクのうち 6つについては、8 ビット グループから 6 ビット グループへの変換を説明する際に既に説明しました。
これらのマスクの一部は、デコード プロセスで 6 ビット グループから 8 ビット グループに変換するために使用されます。さらに、さらに 2つのマスクが必要になります。 全部で8枚のマスクがあります。
typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c
次に、エンコード機能について考えます。 3キャラセットでお作りします。
次に、それらをビット演算を使用して 4 文字のグループに変換します。詳細については、既に説明しました。 最後に、4 文字のグループの各バイトを変換して連結し、エンコードされた文字列を作成します。
コードは次のとおりです。
string encode_base64(UC const* buf, UI bufLen) {
string encoded = "";
UI i = 0, j = 0, k = 0;
UC temp_a_3[3], temp_4[4];
for (i = 0; i < bufLen; i += 3) {
for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
for (; k < 3; k++) temp_a_3[k] = '\0';
temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
temp_4[3] = temp_a_3[2] & MASK6;
for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
encoded += base64_chars[temp_4[k]];
for (; k < 4; k++) encoded += EXTRA; // sentinal value
}
return encoded;
}
この関数は 2つのパラメーターを受け取ります。1つ目は生データ (コーディングのために送信される) で、2つ目はメッセージの長さです。 サイズ 3 と 4 の 2つの配列を宣言しました。ループ内では、サイズ 3 の最初の配列にデータを格納します。
次に、最後のセットのバイト数が少ない場合は、null 文字を追加して最後のセットを完成させます。 次に、8 ビット データを 6 ビット単位の操作に変換する 4つのステートメントがあります。
最後に、最後から 2 番目のループで、4つの 6 ビット文字のグループを base_64 に変換します。
最後のループでは、余分な文字を格納して 4 バイトのセットを完成させます。 次に、decode 関数があります。
vector<UC> decode_base64(string const& encoded) {
UI i = 0, j = 0, k = 0, in_len = encoded.size();
UC temp_a_3[3], temp_4[4];
vector<UC> decoded;
for (i = 0; i < in_len; i += 4) {
for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
temp_4[k++] = base64_chars.find(encoded[j]);
for (; k < 4; k++) temp_4[k++] = '\0';
temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
decoded.push_back(temp_a_3[k]);
}
return decoded;
}
この関数は、エンコードされたメッセージを受け取り、次の手順を含む逆の操作を行います。
base_64文字セットから取得した各文字のインデックスを取得し、4 バイトのセットを作成します。- ここでも、エンコード プロセスで保存した特殊文字に対して 0 を追加します。
- 次に、逆ビット操作で 4 バイトのセットを 3 バイトのセットに変換します (ここでは、これらの操作の詳細には立ち入りません)。
- 最後に、3 バイトのセットを結合して、結合されたデコードされたメッセージを取得します。
最後に、コーディングとエンコーディングの 2つの例を含む完全なコードを示します。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c
const string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
string encode_base64(UC const* buf, UI bufLen) {
string encoded = "";
UI i = 0, j = 0, k = 0;
UC temp_a_3[3], temp_4[4];
for (i = 0; i < bufLen; i += 3) {
for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
for (; k < 3; k++) temp_a_3[k] = '\0';
temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
temp_4[3] = temp_a_3[2] & MASK6;
for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
encoded += base64_chars[temp_4[k]];
for (; k < 4; k++) encoded += EXTRA; // sentinal value
}
return encoded;
}
vector<UC> decode_base64(string const& encoded) {
UI i = 0, j = 0, k = 0, in_len = encoded.size();
UC temp_a_3[3], temp_4[4];
vector<UC> decoded;
for (i = 0; i < in_len; i += 4) {
for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
temp_4[k++] = base64_chars.find(encoded[j]);
for (; k < 4; k++) temp_4[k++] = '\0';
temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
decoded.push_back(temp_a_3[k]);
}
return decoded;
}
int main() {
vector<UC> myData = {'6', '7', '8', '9'};
string encoded = encode_base64(&myData[0], myData.size());
cout << "Encoded String: " << encoded << '\n';
vector<UC> decoded = decode_base64(encoded);
cout << "Decoded Data: ";
for (int i = 0; i < decoded.size(); i++) cout << (char)decoded[i] << ' ';
cout << '\n';
myData = {4, 16, 64};
encoded = encode_base64(&myData[0], myData.size());
cout << "Encoded String: " << encoded << '\n';
decoded = decode_base64(encoded);
cout << "Decoded Data: ";
for (int i = 0; i < decoded.size(); i++) cout << (int)decoded[i] << ' ';
cout << '\n';
return 0;
}
主に、2つのデータセットがあります。 最初のセットには数字があります。 次のセットには数値があります。 したがって、最後のループでは、型を整数にキャストしてデコードされたメッセージを出力します。
出力:
Encoded String: Njc4OQ^^
Decoded Data: 6 7 8 9
Encoded String: BBBA
Decoded Data: 4 16 64
最初のセットは 4 文字 (4 バイト) で、エンコードされたメッセージ Njc4OQ^^ は 6 文字です (最後の 2 文字は余分です)。 2 番目のセットには 3 バイトがあり、エンコードされたメッセージ BBBA には 4 バイトがあります。
base_64 エンコーディングでは、すべての文字に 1 に設定された最大 6 ビットがあり、対応する 64 個の base_64 プライマリ文字があります。 同様に、エンコードされたメッセージには、ASCII よりも 33% 多くのストレージが必要です。
追加のストレージにもかかわらず、利点は特殊文字を管理できることです。 したがって、このエンコード スキームはデータを転送し、完全性を維持します。