前言
Base64是由64个字符的字母表定义的基数为64的编码/解码方案,可以将二进制数据转换为字符传输,是网络上最常见的用于传输8Bit字节码的编码方式之一。注意:采用Base64编码具有不可读性,需要解码后才能阅读。
目前Base64被广泛应用于计算机的各个领域,由于不同场景下对特殊字符的处理(+,/)不同,因此又根据应用场景又出现了Base64的各种改进的“变种”。因此在使用时,必须先确认使用的是哪种encoding类型,才能正确编/解码。
编/解码原理
base64编码基本规则:
把每3个8Bit的字节转换为4个6Bit的字节(3 x 8 = 4 x 6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。不足3个的byte,根据具体规则决定是否填充。
6bit意味着总共有2^6即64种情况,与base64的字符表可以一一对应。
解码则是一个反向的过程,则需要将每4个byte的数据根据base64转换表,转换为3个byte的数据。
具体Encoding类型
不同的语言中实现过程及Encoding的种类,可能并不一致,本文中主要以Golang的实现为例。
以Go encoding/base64 package为例,根据使用的特殊字符及是否填充,具体分为以下四种类型。
StdEncoding
适用环境:标准环境
根据RFC 4648标准定义实现,包含特殊字符'+'
、'/'
,不足的部分采用'='
填充,根据规则,最多有2个'='
。
const encodeStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
// StdEncoding is the standard base64 encoding, as defined in
// RFC 4648.
var StdEncoding = NewEncoding(encodeStd)
URLEncoding
适用环境:url传输
因为URL编码器会把标准Base64中的'/'
和'+'
字符变为形如"%XX"
的形式,而这些"%"
号在存入数据库时还需要再进行转换,因此采用'-'
、'_'
代替'/'
、'+'
,不足的部分采用'='
填充,根据规则,最多有2个'='
。
const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
// URLEncoding is the alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
var URLEncoding = NewEncoding(encodeURL)
RawStdEncoding
除了不填充'='
外,与StdEncoding一致。
// RawStdEncoding is the standard raw, unpadded base64 encoding,
// as defined in RFC 4648 section 3.2.
// This is the same as StdEncoding but omits padding characters.
var RawStdEncoding = StdEncoding.WithPadding(NoPadding)
RawURLEncoding
除了不填充'='
外,与URLEncoding一致。
// RawURLEncoding is the unpadded alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
// This is the same as URLEncoding but omits padding characters.
var RawURLEncoding = URLEncoding.WithPadding(NoPadding)
整体比较
Encoding | 特殊字符 | 是否填充 |
---|---|---|
StdEncoding | '+' 、'/' | 是 |
RawStdEncoding | '+' 、'/' | 否 |
URLEncoding | '-' 、'_' | 是 |
RawURLEncoding | '-' 、'_' | 否 |
编/解码实现
编码-Encode
整体实现代码如下:
func (enc *Encoding) Encode(dst, src []byte) {
if len(src) == 0 {
return
}
// enc is a pointer receiver, so the use of enc.encode within the hot
// loop below means a nil check at every operation. Lift that nil check
// outside of the loop to speed up the encoder.
_ = enc.encode
di, si := 0, 0
n := (len(src) / 3) * 3
for si < n {
// Convert 3x 8bit source bytes into 4 bytes
// 将3x8 bit转换为4bytes
val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])//通过移位运算实现,前8位为src[si+0],中间8位为src[si+1],最后8位为src[si+2]
dst[di+0] = enc.encode[val>>18&0x3F]
dst[di+1] = enc.encode[val>>12&0x3F]
dst[di+2] = enc.encode[val>>6&0x3F]
dst[di+3] = enc.encode[val&0x3F]
si += 3
di += 4
}
remain := len(src) - si
if remain == 0 {
return
}
// Add the remaining small block
val := uint(src[si+0]) << 16
if remain == 2 {
val |= uint(src[si+1]) << 8
}
dst[di+0] = enc.encode[val>>18&0x3F]
dst[di+1] = enc.encode[val>>12&0x3F]
switch remain {//填充
case 2:
dst[di+2] = enc.encode[val>>6&0x3F]
if enc.padChar != NoPadding {
dst[di+3] = byte(enc.padChar)
}
case 1:
if enc.padChar != NoPadding {
dst[di+2] = byte(enc.padChar)
dst[di+3] = byte(enc.padChar)
}
}
}
编码转换
根据规则,是先将3个8bit的数据拆成4个6bit,再将每个6bit高位填充两个0,即变成4个base64字符。核心实现:
val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uin(src[si+2])//通过移位运算实现,前8位为src[si+0],中间8位src[si+1],最后8位为src[si+2]
dst[di+0] = enc.encode[val>>18&0x3F]
dst[di+1] = enc.encode[val>>12&0x3F]
dst[di+2] = enc.encode[val>>6&0x3F]
dst[di+3] = enc.encode[val&0x3F]
由具体实现可以发现源码采用了非常巧妙的方式实现规则:
-
选取前3位byte,分别左移16、8、0位,然后进行逻辑或,得到的结果的前8位、中8位和最后8位分别对应原始的3个byte数据,如此组成新的24 bit数据val。
-
由val数据分别右移18、12、6、0,可以得到前6、12、18、24位数据,所有目前的数据均在后低6位中。
-
为获取低6位,与0x3F(00111111)进行逻辑与&操作即可。
-
根据得到的结果,查找在encode得到对应位置上的字符。
填充
每3个byte进行相关的4byte转换,当有剩余的byte不足3个时,此时如果需要填充,缺几个byte则补几个'='
。
解码-Decode
整体实现代码如下:
func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
if len(src) == 0 {
return 0, nil
}
// Lift the nil check outside of the loop. enc.decodeMap is directly
// used later in this function, to let the compiler know that the
// receiver can't be nil.
_ = enc.decodeMap
si := 0
for strconv.IntSize >= 64 && len(src)-si >= 8 && len(dst)-n >= 8 {
if dn, ok := assemble64(//是否有效的base64字符
enc.decodeMap[src[si+0]],
enc.decodeMap[src[si+1]],
enc.decodeMap[src[si+2]],
enc.decodeMap[src[si+3]],
enc.decodeMap[src[si+4]],
enc.decodeMap[src[si+5]],
enc.decodeMap[src[si+6]],
enc.decodeMap[src[si+7]],
); ok {
binary.BigEndian.PutUint64(dst[n:], dn)
n += 6
si += 8
} else {
var ninc int
si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
n += ninc
if err != nil {
return n, err
}
}
}
for len(src)-si >= 4 && len(dst)-n >= 4 {
if dn, ok := assemble32(
enc.decodeMap[src[si+0]],
enc.decodeMap[src[si+1]],
enc.decodeMap[src[si+2]],
enc.decodeMap[src[si+3]],
); ok {
binary.BigEndian.PutUint32(dst[n:], dn)
n += 3
si += 4
} else {
var ninc int
si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
n += ninc
if err != nil {
return n, err
}
}
}
for si < len(src) {
var ninc int
si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
n += ninc
if err != nil {
return n, err
}
}
return n, err
}
整体处理过程:
-
先进行8转6,依次取前8个字符,根据decodeMap查找对应的字符位置(转换后值),确定全部字节是否是有效的(无效值为0xff)
-
进行8转6操作,如果成功,将获取的前6个byte存入dst中;如果失败,返回之前的解析结果及错误。
-
不足8个的部分进行4转3操作,与8转6处理逻辑一致,成功则将获取的前3个byte存入dst中;如果失败,返回之前的解析结果及错误。
8转6处理过程
关键代码如下:
func assemble64(n1, n2, n3, n4, n5, n6, n7, n8 byte) (dn uint64, ok bool) {
// Check that all the digits are valid. If any of them was 0xff, their
// bitwise OR will be 0xff.
if n1|n2|n3|n4|n5|n6|n7|n8 == 0xff {
return 0, false
}
return uint64(n1)<<58 |
uint64(n2)<<52 |
uint64(n3)<<46 |
uint64(n4)<<40 |
uint64(n5)<<34 |
uint64(n6)<<28 |
uint64(n7)<<22 |
uint64(n8)<<16,
true
}
func (bigEndian) PutUint64(b []byte, v uint64) {
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 56)
b[1] = byte(v >> 48)
b[2] = byte(v >> 40)
b[3] = byte(v >> 32)
b[4] = byte(v >> 24)
b[5] = byte(v >> 16)
b[6] = byte(v >> 8)
b[7] = byte(v)
}
具体解释:
-
根据编码表获取字符的位置,其对应byte的前两位为0。解码是为了获取后6bit数据,因此我们依次将n1、n2、…、n8移位42、36、…、0位即可得到原6bit组成的数据。
-
获取后新的数据仅有48位,即6个byte,我们在低位填充16位,即再左移16位,在转换为uint64后,获取的前6byte即为原始数据。
-
因此n1、n2、…、n8的总移位数58、22、…、16。
4转3与8转6完全原理一致,只是使用uint32转换,此处不再复述。
包含无效字符的处理
func (enc *Encoding) decodeQuantum(dst, src []byte, si int) (nsi, n int, err error) {
// Decode quantum using the base64 alphabet
var dbuf [4]byte
dlen := 4
// Lift the nil check outside of the loop.
_ = enc.decodeMap
for j := 0; j < len(dbuf); j++ {
if len(src) == si {//解析到最后
switch {//说明是
case j == 0:
return si, 0, nil
case j == 1, enc.padChar != NoPadding:
return si, 0, CorruptInputError(si - j)
}
dlen = j
break
}
in := src[si]
si++
out := enc.decodeMap[in]
if out != 0xff {//获取合法字符至dbuf中
dbuf[j] = out
continue
}
if in == '\n' || in == '\r' {//换行跳过不处理
j--
continue
}
if rune(in) != enc.padChar {//如果是非法字符且不是填充,报错处理
return si, 0, CorruptInputError(si - 1)
}
// We've reached the end and there's padding
switch j {//填充处理
case 0, 1://填充不能出现在最后四位的前两位
// incorrect padding
return si, 0, CorruptInputError(si - 1)
case 2:
// "==" is expected, the first "=" is already consumed.
// skip over newlines
// 判断下一个字符
// 忽略换行符
for si < len(src) && (src[si] == '\n' || src[si] == '\r') {
si++
}
// 第三位出现填充符,则必然第四位是填充符,否则非法
if si == len(src) {
// not enough padding
return si, 0, CorruptInputError(len(src))
}
if rune(src[si]) != enc.padChar {
// incorrect padding
return si, 0, CorruptInputError(si - 1)
}
si++
}
// 填充符出现在第四位,意味着到达结束点,则其后除非是换行,否则均为非法
// skip over newlines
for si < len(src) && (src[si] == '\n' || src[si] == '\r') {
si++
}
// 填充符后
if si < len(src) {
// trailing garbage
err = CorruptInputError(si)
}
dlen = j //2、3
break
}
// Convert 4x 6bit source bytes into 3 bytes
// 进行4转3,dbuf中可能并非完整的存入合法的4个字符,可能存在0-2个填充,这些位置的byte为0,因此不能复用之前合法字符的转换方式。
// 依次多左移6位再右移8位,得到的前3个byte即为原始值
// 填充的byte为0,转换后依然为0
val := uint(dbuf[0])<<18 | uint(dbuf[1])<<12 | uint(dbuf[2])<<6 | uint(dbuf[3])
dbuf[2], dbuf[1], dbuf[0] = byte(val>>0), byte(val>>8), byte(val>>16)
// 根据填充的个数,依次将获取的byte放入dst中
switch dlen {
case 4://没有填充
dst[2] = dbuf[2]
dbuf[2] = 0
fallthrough
case 3://有一个填充符
dst[1] = dbuf[1]
// 如果存在一个填充,则dbuf[2]必然为0,否则就不是填充了
if enc.strict && dbuf[2] != 0 {
return si, 0, CorruptInputError(si - 1)
}
dbuf[1] = 0
fallthrough
case 2://有两个填充符
dst[0] = dbuf[0]
// 如果存在2个填充,则dbuf[1]、dbuf[2]必然为0,否则就不是填充了
if enc.strict && (dbuf[1] != 0 || dbuf[2] != 0) {
return si, 0, CorruptInputError(si - 2)
}
}
return si, dlen - 1, err
}
包含无效字符的处理方式是:
-
预定义4byte dbuf数组,用以存入获取到的合法字符
-
获取si位置的字符,查找decodeMap
- 若是合法字符,存入debuf,查找下一个字符
- 若是换行符,跳过不处理
- 若是非法字符,且不是不是填充字符,报错退出。
- 若是填充字符,进行填充位置的判断,
- 填充只可能出现在4个byte的后2个位置,否则不合法,报错退出
- 若第三个位置是填充符,则后续的字符,除换行符外,则必然是填充符,否则不合法,报错退出。
- 若第四位是填充符,后续到src末端,除换行符外,不应该存在其他字符。
- 没有填充,dlen默认为4,有一个填充,dlen位,两个填充,dlen为2
- 若当前位置已到达src末端
- 若未获取到一个合法字符,则意味着,src当前中无有效信息需要解码,正常退出。
- 若仅获取到一个字符,且encoding是需要填充的(填充最多2位),则意味着src非法,报错退出。
-
根据debuf进行4转3处理(dbuf中可能并非完整的存入合法的4个字符,未存入的位置的byte为0,需要根据合法值的数量,确认数据的位置)
-
依次多左移6位再右移8位,得到的前3个byte(0左/右移后仍为0)
-
根据dlen(填充数量= 4-dlen)的确认数据,从低位到高位依次处理
- 没有填充(或不填充时全部为有效值),取全部值,dst[2] = dbuf[2],继续
- 有1个填充(或不填充时仅有2个有效值),取前2个值,dst[1] = dbuf[1];
- 如果采用strict模式,则dbuf[2]必然为0,正常继续;否则异常,退出。
- 有2个填充(或不填充时仅有1个有效值),取第一个值,dst[0] = dbuf[0];
- 如果采用strict模式,则dbuf[1]、dbuf[2]必然为0,正常继续;否则异常,退出。
-
总结
本文主要以Go base64 package为例,详细介绍了4种Encoding的异同点及使用环境,同时对base64编/解码的详细实现过程进行了较深入的探讨。
Encoding的不同主要是因为使用环境对特殊字符的处理导致,如url传输就需要使用相关的URLEncoding,若使用StdEncoding会导致'+'
、'/'
符号异常。
编解码中则充分利用了左移、右移的特性及uint32、uint64与byte的转换,在简短的代码中即实现了byte数据的转换,而无需按照规则中对具体的bit位进行操作。这点可以给我们很多启示,在以后的代码中,不妨做下相关的思考,有没有更简单的方式实现。
公众号
鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。