Base64 编码原理及实现

2021/11/30 计算机原理

参考 RFC4648

以上的 RFC 描述了几种常用的编码方案:Base64,Base32 和 Base16。数据的 Base 编码用于多种存储或者传输场景中,例如图片的传输,其中以 Base64 尤为常见。

Base64 编码

Base64 编码使用了 US-ASCII 的一个子集 —— 65 个字符,将每 6 位数据都表示成一个可打印字符,额外的第 65 个字符=表示一种特殊的处理。编码的过程 —— 用 4 个作为输出的编码字符表示 1 个 24 位输入组。1 个 24 位输入组由 3 个连续的 8 位输入组组成,这 24 位的输入组又被当作 4 个 6 位的输出组(每个输出位组都被翻译成 1 个单独的 Base64 编码字符)。

每 1 个 6 位输出组都是 64 个可打印字符的索引值。例如000000(值 0)表示A。Base64 编码字符如下表所示:

字符 字符 字符 字符
A 0 Q 16 g 32 w 48
B 1 R 17 h 33 x 49
C 2 S 18 i 34 y 50
D 3 T 19 j 35 z 51
E 4 U 20 k 36 0 52
F 5 V 21 l 37 1 53
G 6 W 22 m 38 2 54
H 7 X 23 n 39 3 55
I 8 Y 24 o 40 4 56
J 9 Z 25 p 41 5 57
K 10 a 26 q 42 6 58
L 11 b 27 r 43 7 59
M 12 c 28 s 44 8 60
N 13 d 29 t 45 9 61
O 14 e 30 u 46 + 62
P 15 f 31 v 47 / 63

如果要输入数据剩下的位数少于 24,就会进行特殊的处理(位0会被添加到数据的右边以使位数是 6 位组的整数倍),并在数据的末端使用=字符填充。

  1. 输入数据是 24 位的整数倍,编码的结果就是 4 个字符的整数倍,不会存在填充的=字符。
  2. 输入数据最后剩下 8 位,编码后会在最后填充 2 个=字符(注:将用 0 填充后的数据的前面 12 位所表示的字符写入之后再写入 2 个=字符)。
  3. 输入数据最后剩下 16 位,编码后会在最后填充 1 个=字符(注:将用 0 填充后的数据的前面 18 位所表示的字符写入之后再写入 1 个=字符)。

URL 和文件名安全的 Base64 编码

一种可选的建议就是使用~作为第 63 个字符。而~字符在某些文件系统中有特殊含义。剩下的非保留的 URI 字符就是.,但某些文件系统不允许在文件名在有多个.,因此.也不可用。

填充字符=在 URI 中会被%编码,但如果数据长度是已知的,就可以避免填充。

该编码可能会被称为base64url,它不应该被认为跟 Base64 编码一样,也不应该被认为它仅仅就是 Base64。除非特别说明,不然 Base64 就是指 Base64 编码。

除了第 62 和第 63 个字符之外,该编码在技术上完全等同于 Base64。以下就是 URL 和 文件名安全的 Base64 字符表:

字符 字符 字符 字符
A 0 Q 16 g 32 w 48
B 1 R 17 h 33 x 49
C 2 S 18 i 34 y 50
D 3 T 19 j 35 z 51
E 4 U 20 k 36 0 52
F 5 V 21 l 37 1 53
G 6 W 22 m 38 2 54
H 7 X 23 n 39 3 55
I 8 Y 24 o 40 4 56
J 9 Z 25 p 41 5 57
K 10 a 26 q 42 6 58
L 11 b 27 r 43 7 59
M 12 c 28 s 44 8 60
N 13 d 29 t 45 9 61
O 14 e 30 u 46 - 62
P 15 f 31 v 47 _ 63

Base64 编码实现

以下代码由 Go 语言的标准库 base64 修改而来。

package main

import (
    "bytes"
    "encoding/base64"
    "fmt"
)

const stdBase64Char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
const stdPad = '='

func EncodeStringToString(src string) string {
    if len(src) == 0 {
        return ""
    }

    dst := bytes.NewBuffer(nil)
    fullLen := (len(src) / 3) * 3
    si, di := 0, 0
    for ; si < fullLen; si, di = si+3, di+4 {
        val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])
        dst.Write([]byte{
            stdBase64Char[val>>18&0x3f],
            stdBase64Char[val>>12&0x3f],
            stdBase64Char[val>>6&0x3f],
            stdBase64Char[val&0x3f],
        })
    }

    remain := len(src) - fullLen

    if remain == 0 {
        return dst.String()
    }

    val := uint(src[si+0]) << 16
    if remain == 2 {
        val |= uint(src[si+1]) << 8
    }

    dst.Write([]byte{
        stdBase64Char[val>>18&0x3F],
        stdBase64Char[val>>12&0x3F],
    })

    switch remain {
    case 2:
        dst.Write([]byte{
            stdBase64Char[val>>6&0x3f],
            stdPad,
        })
    case 1:
        dst.Write([]byte{
            stdPad,
            stdPad,
        })
    }

    return dst.String()
}

func main() {
    raw := "Hello world"
    //raw = ""
    //raw = "Hello worl"
    //raw = "Hello worldA"
    //raw = "这是中文啊"
    //raw = "这是中文啊a"
    //raw = "这是中文啊ab"
    fmt.Println(base64.StdEncoding.EncodeToString([]byte(raw)))
    fmt.Println(EncodeStringToString(raw))
}

Search

    Table of Contents