C++에서 Base 64 인코딩 구현

Abdul Mateen 2023년10월12일
  1. 인코딩 방식 Base_64
  2. C++에서 Base_64 인코딩 구현
C++에서 Base 64 인코딩 구현

이 튜토리얼에서는 C++에서 base_64의 인코딩에 대해 설명합니다.

먼저 base_64 인코딩과 이것이 필요한 이유와 위치에 대해 설명합니다. 나중에 C++에서 base_64 인코딩/디코딩에 대해 논의할 것입니다.

인코딩 방식 Base_64

‘Base_64’는 인코딩 체계에 추가된 것입니다. 이진 데이터를 ASCII 문자열로 표현한다는 점에서 이진-텍스트 인코딩과 유사합니다.

차이점은 base_64 인코딩이 radix-64로의 변환을 사용한다는 것입니다. Base_64 인코딩 이름은 base의 수학적 정의에서 따온 것입니다.

밑수는 숫자 체계의 기본 자릿수를 나타냅니다. base_2와 마찬가지로 기본 숫자는 0과 1입니다.

8진수 체계인 ‘Base_8’은 0부터 7까지 8개의 기본 숫자가 있습니다.

마찬가지로 base_16에는 0에서 15까지 16개의 기본 숫자가 있으며 여기서 A에서 F를 사용하여 10에서 15를 나타냅니다. base_64에는 다음으로 구성된 64개의 기본 숫자가 있습니다.

  1. 26자 대문자
  2. 26개의 소문자
  3. 10자리, 0~9
  4. +/ 기호 2개

‘Base_64’ 인코딩은 일반적으로 미디어를 통해 데이터를 전송하는 데 사용되며 ASCII를 처리하도록 설계되었습니다. ‘base_64’는 미디어를 통해 전송되는 데이터의 무결성을 유지하려고 합니다.

주요 응용 프로그램은 MIME을 통한 이메일과 XML에 복잡한 데이터를 저장하는 것입니다. Base_64는 프라이버시 강화 전자 메일(PEM)이라고도 합니다.

Base_64 인코딩 단계

인코딩을 위해 우리는 문자열의 각 문자에 대해 작업을 수행해야 하는 이진 문자열로 데이터를 가지고 있습니다. base_64에서 인코딩하려면 다음 단계를 수행해야 합니다.

  • 각 문자의 ASCII 값을 가져옵니다.
  • ASCII 값의 8비트 이진수를 찾으십시오.
  • 숫자를 재배열하여 8비트(2단계에서 얻은)를 6비트로 변환합니다(일부 비트 연산을 포함한 일부 조작 필요(나중에 설명)).
  • 6비트 바이너리를 해당 십진수 값으로 변환
  • base_64(이미 설명한 base_64의 기본 숫자)를 사용하여 각 십진수 값에 해당하는 base_64 문자를 할당합니다.

여기서는 8비트 그룹에서 6비트 그룹으로 변환하는 3단계에 대해 자세히 설명합니다.

8비트 그룹을 6비트 그룹으로 변환하는 절차

Base_64 인코딩에서는 처음에 논의한 바와 같이 64 기본 문자/숫자가 있지만 일반적으로 데이터를 바이트 단위로 읽고 씁니다. 1바이트는 8비트로, 0에서 255까지 저장할 수 있습니다. 즉, 바이트에서 고유한 256을 나타낼 수 있습니다.

6비트는 64 고유 값을 나타낼 수 있으며 각 바이트가 Base_64 인코딩 체계의 1자리/문자만 저장하도록 마지막 2비트를 0으로 유지해야 합니다.

각 문자/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바이트로 그룹화됩니다. t1, t2 & t3으로 3바이트 세트를, f1, f2, f3 & f4로 4바이트 세트를 고려하십시오.

f1 = ( t1 & 0xfc ) >> 2

마스크 0xfc(바이너리 11111100에 해당)를 고려하고 집합의 첫 번째 바이트와 마스크 사이에 비트 단위 및 연산을 적용합니다. 다음으로 오른쪽 시프트를 두 번 사용하여 비트별 연산 결과를 구합니다.

시프트 연산은 왼쪽 6비트를 오른쪽으로 옮기고 마지막 2비트는 0이 됩니다.

마스크 0xfc에는 처음 2비트 0이 있습니다. 작업이 집합의 첫 번째 바이트의 처음 2비트를 0으로 만드는 경우(즉, 첫 번째 바이트의 마지막 6비트가 고려됨), 처음 2비트(이 작업에서 무시됨)는 다음 프로세스에서 고려됩니다.

f2 = ( ( t1 & 0x03 ) << 4 ) + ( ( t2 & 0xf0 ) >> 4 )

여기서 0x03 00000011 마스크는 작업의 첫 번째 바이트에 적용됩니다(즉, 처음 2비트만 고려되고 마지막 6비트는 이전 작업에서 이미 고려됨을 의미합니다). 시프트 연산은 첫 번째 바이트의 결과 2비트를 왼쪽으로 전송하여 표현식의 다섯 번째 및 여섯 번째 비트로 만듭니다.

마스크 0xf0 11110000은 작업의 두 번째 바이트에 적용됩니다(마지막 4비트만 고려됨을 의미). 시프트 연산은 결과 4비트를 오른쪽으로 전송하여 표현식의 처음 4비트로 만듭니다.

표현식의 첫 번째 부분은 5번째 및 6번째 비트가 켜져 있고, 두 번째 부분은 처음 4비트가 켜져 있고, 집합적으로 처음 6비트가 켜져 있고 마지막 비트가 꺼져 있습니다.

마지막으로 이들을 결합하여 마지막 2비트가 꺼진 바이트를 얻습니다. 이 단계에서 우리는 첫 번째 바이트가 완성되고 두 번째 바이트의 처음 4비트가 고려되는 6비트의 또 다른 바이트를 얻었습니다.

f3 = ( ( t2 & 0x0f ) << 2 ) + ( ( t3 & 0xc0 ) >> 6 )

마스크 0x0f 00001111은 연산을 위해 두 번째 바이트에 적용됩니다(즉, 처음 4비트만 고려됨). 시프트 연산은 결과 4비트를 왼쪽으로 전송하여 표현식의 세 번째, 네 번째, 다섯 번째 및 여섯 번째 비트로 만들고 처음 2비트를 위한 공간을 만듭니다.

다음으로 마스크 0xc0 11000000이 연산을 위해 세 번째 바이트에 적용됩니다(즉, 처음 2비트만 고려됨). 시프트 연산은 결과 2비트를 오른쪽으로 전송하여 식의 첫 번째 및 두 번째 비트로 만듭니다.

마지막으로 두 결과가 결합되어 6비트 그룹의 세 번째 바이트를 얻습니다. 다시 세트에서 세트의 두 번째 바이트와 세 번째 바이트의 2비트를 완료했습니다.

f4 = t3 & 0x3f

마지막으로 세 번째 바이트에는 작업만 있으며 마스크 0x3f 00111111에는 처음 6비트가 켜져 있고 마지막 2비트가 꺼져 있습니다. 세 번째 바이트를 사용한 작업은 세 번째 바이트의 나머지 6비트를 고려합니다.

우리는 이미 base_64에서 사용되는 64개의 기본 숫자에 대해 논의했습니다. 다음 단계에서 4바이트 집합의 각 바이트(비트 연산을 사용하여 얻음)는 base_64로 변환되고 문자열로 연결됩니다.

PLAY라는 단어를 인코딩해 보겠습니다. 첫 번째 단계에서는 각각 3개의 캐릭터가 있는 세트를 만듭니다. 첫 번째 세트에는 PLA가 있습니다.

다음 단계에는 Y\0\0이 있습니다. 여기서 \0은 집합을 완성하기 위해 추가된 null 문자입니다.

각 문자의 ASCII는 80 76 65 89 0 0입니다. 해당 이진 값은 01010000 01001000 01000001 01011001입니다.

이제 비트 연산을 해봅시다.

  1. f1 = 01010000 & 11111100 = 01010000 >> 2 = 00010100 = 20
  2. 01010000 & 00000011 = 0000000 << 4 = 00000000 식의 첫 부분
  3. 01001000 & 11110000 = 01010000 >> 4 = 00000101 표현식의 두 번째 부분
  4. f2 = 00000000 + 00000101 = 00000101 = 5, 첫 번째 부분과 두 번째 부분의 결과를 더합니다.
  5. 01001000 & 00001111 = 00001000 << 2 = 00100000 식의 첫 부분
  6. 01000001 & 11000000 = 01000000 >> 4 = 00000100 표현식의 두 번째 부분
  7. f3 = 00100000 + 00000100 = 00100100 = 36, 첫 번째 부분과 두 번째 부분의 결과를 더합니다.
  8. f4 = 01000001 & 00000011 = 00000001 = 1

이제 두 번째와 세 번째 값이 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개의 매개변수를 사용합니다. 첫 번째는 원시 데이터(코딩을 위해 전송됨)이고 두 번째는 메시지의 길이입니다. 크기가 3과 4인 2개의 배열을 선언했습니다. 루프 내부에서 크기가 3인 첫 번째 배열에 데이터를 저장합니다.

다음으로 마지막 세트의 바이트가 적은 경우 null 문자를 추가하여 마지막 세트를 완성합니다. 다음으로 8비트 데이터를 6비트 단위 연산으로 변환하는 4개의 명령문이 있습니다.

마지막으로 두 번째에서 마지막 루프까지 4개의 6비트 문자 그룹을 base_64로 변환합니다.

마지막 루프는 추가 문자를 저장하여 4바이트 세트를 완성합니다. 다음으로 디코딩 기능이 있습니다.

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;
}

이 함수는 인코딩된 메시지를 가져와 다음 단계를 포함하는 반대 작업을 수행합니다.

  1. base_64 문자 집합에서 얻은 각 문자의 인덱스를 가져와서 4바이트 집합을 만듭니다.
  2. 다시 인코딩 프로세스에서 저장한 특수 문자에 대해 0을 추가합니다.
  3. 다음으로 반전 비트 연산을 통해 4바이트 세트를 3바이트 세트로 변환합니다(여기서는 이러한 연산에 대해 자세히 다루지 않습니다).
  4. 마지막으로 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자는 추가 문자임)가 있습니다. 두 번째 세트에는 3바이트가 있고 인코딩된 메시지 BBBA에는 4바이트가 있습니다.

base_64 인코딩에서 모든 문자는 1로 설정된 최대 6비트를 가지며, 여기서 해당 64 base_64 기본 문자가 있습니다. 마찬가지로 인코딩된 메시지는 ASCII보다 33% 더 많은 저장 공간이 필요합니다.

추가 저장 공간에도 불구하고 장점은 특수 문자를 관리하는 것입니다. 따라서 이 인코딩 체계는 데이터를 전송하고 무결성을 그대로 유지합니다.