前言
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開發相關內容,望大家感興趣的支持一下,在此特別感謝。