Base64 编码算法解析与实现

236 阅读5分钟

1. 前言

Base64 是一种将二进制数据转换为可打印字符的编码方案,广泛应用于电子邮件、网页传输等场景。它通过将每 3 个字节的二进制数据编码为 4 个 ASCII 字符,确保数据在文本环境中可靠传输。之前都是用Python实现,很久没用C语言了,今天复习一下

(RFC 文档读起来像说明书,但是核心思路就两个词:分组 + 映射

2. 技术简介

Base64 编码遵循 RFC 4648 标准,核心原理如下:

  1. 将输入数据按 3 字节分组;

  2. 将每组 24 位拆分为 4 个 6 位片段;

  3. 将每个 6 位片段映射到 Base64 字符表;

  4. 不足 3 字节时补零并用 = 填充

编码后的数据长度固定为原数据长度的 4/3 倍(向上取整)

3. 算法原理讲解

3.1 数据分组与补零

  • 将输入数据分割为多个 3 字节组;

  • 最后一组不足 3 字节时补零:

    • 1 字节剩余:补 2 字节零 → 生成 2 个数据字符 + 2 个 =

    • 2 字节剩余:补 1 字节零 → 生成 3 个数据字符 + 1 个 =

(这个时候就有人要问“为什么一定是 3 → 4,而不是 2 → 3?”——因为 24 可被 6 整除)

3.2 位操作与字符映射

  1. 将 3 字节拼接为 24 位整数

  2. 依次取 6 位片段(从高位到低位):

    • 第 1 个字符:取高 6 位(位 23‑18)

    • 第 2 个字符:取中间 6 位(位 17‑12)

    • 第 3 个字符:取次中间 6 位(位 11‑6)

    • 第 4 个字符:取低 6 位(位 5‑0)

3.3 填充处理

根据实际数据长度决定填充字符数量:

数据长度 % 3 == 0 → 无填充
数据长度 % 3 == 1 → 补 2 个 =
数据长度 % 3 == 2 → 补 1 个 =

(要补充一下那两个 = 不是装可爱,是 MIME 时代遗留下来的对齐标记)

4. 代码结构解析

4.1 Base64 映射表

static const char B64_TABLE[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "abcdefghijklmnopqrstuvwxyz"
    "0123456789+/";
  • 包含 64 个标准字符,索引 0‑63 对应不同字符

  • +/ 为最后两个字符

(有时候为了安全,URL‑Safe 版本把 + 换成 -/ 换成 _

4.2 核心编码函数

4.2.1 函数原型

char *base64_encode(const uint8_t *data, size_t len);
  • 输入:二进制数据指针 data 及其长度 len

  • 输出:动态分配的 Base64 字符串(需调用者释放)

4.2.2 内存分配

size_t out_len = (len + 2) / 3 * 4;
char *out = (char *)malloc(out_len + 1);
  • +2 再除以 3:实现向上取整

  • 多申请 1 字节给 \0 终止符

malloc 失败一定要记得判空!)

4.2.3 主处理循环

while (i < len) {
    int remaining = len - i;
    int chunk = remaining >= 3 ? 3 : remaining;

    uint32_t triple = 0;
    triple |= data[i] << 16;
    if (chunk > 1) triple |= data[i+1] << 8;
    if (chunk > 2) triple |= data[i+2];

    out[o++] = B64_TABLE[(triple >> 18) & 0x3F];
    out[o++] = B64_TABLE[(triple >> 12) & 0x3F];
    out[o++] = (chunk > 1) ? B64_TABLE[(triple >> 6) & 0x3F] : '=';
    out[o++] = (chunk > 2) ? B64_TABLE[triple & 0x3F] : '=';

    i += chunk;
}
  • 24 位拼接:移位操作组合最多 3 字节

  • 动态填充:根据 chunk 决定 =

  • 位掩码0x3F 留下 6 位

4.2.4 终止符处理

out[out_len] = '\0';
  • 保证返回值是合法 C 字符串

5. 关键代码解析

5.1 24 位数据拼接

triple |= data[i] << 16;          // 首字节占高位
if (chunk > 1)
    triple |= data[i+1] << 8;     // 次字节居中
if (chunk > 2)
    triple |= data[i+2];          // 末字节占低位
  • <<| 比起 memcpy 更秀位级操作

5.2 动态截断与填充

out[o++] = (chunk > 1) ? ... : '=';  // 第三字符
out[o++] = (chunk > 2) ? ... : '=';  // 第四字符
  • 仅 1 字节:补 ==;仅 2 字节:补 =

5.3 位掩码操作

(triple >> 18) & 0x3F  // 高 6 位
(triple >> 12) & 0x3F  // 次高 6 位
  • 0x3F = 0011 1111,精准抠出 6 bit,这一步要是写 & 63 也行,看个人口味

7. 完整实现代码

#include <stdlib.h>
#include <stdint.h>

static const char B64_TABLE[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "abcdefghijklmnopqrstuvwxyz"
    "0123456789+/";

char *base64_encode(const uint8_t *data, size_t len) {
    size_t out_len = (len + 2) / 3 * 4;
    char *out = (char *)malloc(out_len + 1);
    if (!out) return NULL;

    size_t i = 0, o = 0;
    while (i < len) {
        int remaining = len - i;
        int chunk = remaining >= 3 ? 3 : remaining;

        uint32_t triple = data[i] << 16;
        if (chunk > 1) triple |= data[i + 1] << 8;
        if (chunk > 2) triple |= data[i + 2];

        out[o++] = B64_TABLE[(triple >> 18) & 0x3F];
        out[o++] = B64_TABLE[(triple >> 12) & 0x3F];
        out[o++] = (chunk > 1) ? B64_TABLE[(triple >> 6) & 0x3F] : '=';
        out[o++] = (chunk > 2) ? B64_TABLE[triple & 0x3F] : '=';

        i += chunk;
    }

    out[out_len] = '\0';
    return out;
}

有的人就要问了,老师老师,怎么运行了没用呢,先把这个代码保存为base64.c,然后用下面的代码调用

#include <stdio.h>
#include <string.h>
#include <stdint.h>

char* base64_encode(const uint8_t* data, size_t len);

int main(void)
{
    const char* txt = "hello, world";
    char* encoded = base64_encode((const uint8_t*)txt, strlen(txt));
    if (!encoded) {
        fprintf(stderr, "malloc 失败\n");
        return 1;
    }
    printf("原文: %s\n", txt);
    printf("Base64: %s\n", encoded);
    free(encoded);
    return 0;
}

说在最后

这是第一次介绍一下自己,本人只是个小学语文老师,大学的时候迷恋上写代码,陆陆续续学了点,学艺不精啊,写一些笔记鼓励自己能一直学(虽然不知道学了干嘛。。。。)