Base64
Base64是一種用64個(gè)字符表示任意二進(jìn)制數(shù)據(jù)的方法橡娄。
編碼前诗箍,可以是各種各樣的字符,中文挽唉、法語(yǔ)滤祖、日語(yǔ)等,先把這些字符轉(zhuǎn)成字節(jié)瓶籽,然后對(duì)這些字節(jié)進(jìn)行編碼匠童,編碼后就都是限定了的64個(gè)字符了。
好處是編碼后文本數(shù)據(jù)可以顯示出來(lái)了塑顺,可以用在郵件汤求、網(wǎng)頁(yè)、URL上严拒。
編碼后扬绪,3字節(jié)的二進(jìn)制數(shù)變成了4字節(jié)的文本數(shù)據(jù),長(zhǎng)度增加了33%裤唠,所以適合傳送少量二進(jìn)制數(shù)據(jù)挤牛。
編碼
首先準(zhǔn)備一個(gè)含有64個(gè)字符的數(shù)組,一般是 “A~Za~z0~9+/”种蘸;
然后原先的二進(jìn)制數(shù)據(jù)每3個(gè)字節(jié)一組墓赴,就是3*8=24個(gè)比特,然后分成四組航瞭,每組6個(gè)比特诫硕,每6個(gè)比特就對(duì)應(yīng)了一個(gè)0~63之間的數(shù)字;
把這4個(gè)范圍在0~63的數(shù)字當(dāng)作索引刊侯,在上面的字符數(shù)組里查找相應(yīng)的字符章办;
這樣就是編碼后的字符串了。
特殊處理:
如果要編碼的二進(jìn)制數(shù)據(jù)不是3的倍數(shù)滨彻,就用\x00
字節(jié)在末尾補(bǔ)足藕届,然后再在編碼的末尾加上1到2個(gè)等號(hào)(=
),表示補(bǔ)了多少字節(jié)疮绷,這樣解碼的時(shí)候就可以自動(dòng)去掉了翰舌。
特別注意嚣潜,Base64編碼后的文本的長(zhǎng)度總是4的倍數(shù)冬骚,但是如果再加上1到2個(gè)
=
不就不是4的倍數(shù)了嗎?
所以并不是先編碼,再加上1到2個(gè)=
只冻,而是編碼之后庇麦,把最后的1到2個(gè)字符(這個(gè)字符肯定是A
)替換成=
解碼
與編碼相反,首先去除末尾的等號(hào)(=
)喜德,然后比對(duì)初始的64字符的數(shù)組山橄,把編碼后的文本轉(zhuǎn)成各字符在數(shù)組里的索引值,再然后轉(zhuǎn)成6比特的二進(jìn)制數(shù)舍悯,最后刪除多余的\x00
航棱。
改進(jìn)
- 標(biāo)準(zhǔn)Base64里是包含
+
和/
的,在URL里不能直接作為參數(shù)萌衬,所以出現(xiàn)一種 “url safe” 的Base64編碼饮醇,其實(shí)就是把+
和/
替換成-
和_
。 - 同樣的秕豫,
=
也會(huì)被誤解朴艰,所以編碼后干脆去掉=
,解碼時(shí)混移,自動(dòng)添加一定數(shù)量的等號(hào)祠墅,使得其長(zhǎng)度為4的倍數(shù)即可正常解碼了。
思考
編碼時(shí)歌径,
=
添加的邏輯是:原始二進(jìn)制數(shù)據(jù)的長(zhǎng)度不是3的倍數(shù)時(shí)毁嗦,要補(bǔ)\x00
,補(bǔ)了多少個(gè)\x00
沮脖,就在編碼的末尾添加幾個(gè)=
金矛;
而解碼時(shí),自動(dòng)添加=
的邏輯是:把編碼后的文本的長(zhǎng)度補(bǔ)齊到4的倍數(shù)勺届。
這兩個(gè)的邏輯明明是不同的驶俊,為什么可以工作呢?
解答
因?yàn)檫@里的
=
的意義只在于表示編碼時(shí)添加了多少個(gè)\x00
免姿,哪怕缺失了=
饼酿,也可以知道缺失了多少個(gè),那么在解碼后就要去掉多少個(gè)\x00
胚膊。
而且編碼時(shí)添加的=
其實(shí)是替換故俐,并不會(huì)破壞編碼后長(zhǎng)度為4的倍數(shù)的這個(gè)特性,所以解碼時(shí)紊婉,把=
換成A
药版,然后正常解碼,再刪除掉尾部的X個(gè)\x00
即可(X=1或著2)喻犁。
Base58
除了Base64槽片,還有Base16何缓、Base32、Base58还栓、Base85等編碼方式碌廓,這里介紹一下Base58是因?yàn)椋?/p>
- 它的編碼思路和Base64看起來(lái)不太一樣(雖然實(shí)質(zhì)是一樣的)
- 刪除了
+
/
0
O
I
l
,讓編碼后的結(jié)果更加清晰剩盒,且不容易看錯(cuò) - 比特幣谷婆、Monero、Ripple辽聊、Flickr都在用這個(gè)Base58的編碼方式
Base58的本質(zhì)就是把256進(jìn)制的值轉(zhuǎn)成58進(jìn)制的值纪挎。
所以它在編碼時(shí)不需要考慮補(bǔ)\x00
的問(wèn)題,直接轉(zhuǎn)換即可跟匆。
把字節(jié)流轉(zhuǎn)成一個(gè)256進(jìn)制的大數(shù)廷区,然后不斷除以58,保留余數(shù)贾铝,最后余數(shù)當(dāng)作索引隙轻,再倒序,即為轉(zhuǎn)換后的結(jié)果垢揩。
特殊處理:
不同于一個(gè)普通的數(shù)字轉(zhuǎn)成某個(gè)進(jìn)制玖绿,普通數(shù)字最高位是不會(huì)為0的,而我們要編碼的對(duì)象是字節(jié)流叁巨,那么如果字節(jié)流的最前面是0(\x00
)斑匪,那么就會(huì)丟失這個(gè)信息。所以編碼時(shí)要特殊記錄一下锋勺,字節(jié)流的開端有多少個(gè)\x00
蚀瘸,就直接在轉(zhuǎn)換后的編碼前面加上多少個(gè)b58Alphabet[0]
,同理庶橱,解碼的時(shí)候先記錄一下前面的b58Alphabet[0]
的個(gè)數(shù)贮勃,然后解碼之后再在解碼的前面加上相同數(shù)量的0x00
。
示例代碼
base58.go
var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
// Base58Encode encodes a byte array to Base58
func Base58Encode(input []byte) []byte {
var result []byte
x := big.NewInt(0).SetBytes(input)
base := big.NewInt(int64(len(b58Alphabet)))
zero := big.NewInt(0)
mod := &big.Int{}
for x.Cmp(zero) != 0 {
x.DivMod(x, base, mod)
result = append(result, b58Alphabet[mod.Int64()])
}
ReverseBytes(result)
for _, b := range input {
if b == 0x00 {
result = append([]byte{b58Alphabet[0]}, result...)
} else {
break
}
}
return result
}
// Base58Decode decodes Base58-encoded data
func Base58Decode(input []byte) []byte {
result := big.NewInt(0)
zeroBytes := 0
for _, b := range input {
if b == b58Alphabet[0] {
zeroBytes++
} else {
break
}
}
payload := input[zeroBytes:]
for _, b := range payload {
charIndex := bytes.IndexByte(b58Alphabet, b)
result.Mul(result, big.NewInt(58))
result.Add(result, big.NewInt(int64(charIndex)))
}
decoded := result.Bytes()
decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)
return decoded
}
base58_test.go
func TestBase58Encode(t *testing.T) {
data := []byte{0x00, 0x00, 0x00}
encoded := Base58Encode(data)
assert.Equal(t, []byte{'1', '1', '1'}, encoded)
data = []byte{'a'}
encoded = Base58Encode(data)
assert.Equal(t, []byte{'2', 'g'}, encoded)
data = []byte{'1', 0x00}
encoded = Base58Encode(data)
assert.Equal(t, []byte{'4', 'j', 'H'}, encoded)
}
func TestBase58Decode(t *testing.T) {
data := []byte{0x00, 0x00, 0x00}
assert.Equal(t, data, Base58Decode(Base58Encode(data)))
data = []byte{'a'}
assert.Equal(t, data, Base58Decode(Base58Encode(data)))
data = []byte{'1', 0x00}
assert.Equal(t, data, Base58Decode(Base58Encode(data)))
}
總結(jié)
不管是Base64還是Base58苏章,都會(huì)造成信息的冗余寂嘉,使得需要傳輸?shù)臄?shù)據(jù)量增大,所以不會(huì)用在很大的數(shù)據(jù)上枫绅。
- 使用Base64最普遍的是URL泉孩、郵件文本、圖片并淋;
- 相比于Base64直接切割比特的方法(3個(gè)比特變?yōu)?個(gè)比特)寓搬,Base58采用的大數(shù)進(jìn)制轉(zhuǎn)換,效率更低县耽,所以使用場(chǎng)景的數(shù)據(jù)更少句喷,例如上面提到的比特幣的地址的編碼曼尊。