Base64是最經常使用的編碼之一,好比開發中用於傳遞參數、現代瀏覽器中的<img />標籤直接經過Base64字符串來渲染圖片以及用於郵件中等等。Base64編碼在RFC2045中定義,它被定義爲:Base64內容傳送編碼被設計用來把任意序列的8位字節描述爲一種不易被人直接識別的形式。javascript
目錄[-]java
Base64是最經常使用的編碼之一,好比開發中用於傳遞參數、現代瀏覽器中的<img />標籤直接經過Base64字符串來渲染圖片以及用於郵件中等等。Base64編碼在RFC2045中定義,它被定義爲:Base64內容傳送編碼被設計用來把任意序列的8位字節描述爲一種不易被人直接識別的形式。windows
咱們知道,任何數據在計算機中都是以二進制的方式存儲的。一個字節爲8位,一個字符在計算機中存儲爲一個或多個字節,好比英文字母、數字以及英文標點符號就是用一個 字節來存儲的,一般稱爲ASCII碼。而簡體中文、繁體中文、日文以及韓文等都是用多字節來存儲的,一般稱爲多字節字符。由於Base64編碼是對字符串的編碼表示進行處理的,不一樣編碼的字符串的Base64的結果是不一樣的,因此咱們須要瞭解基本的字符編碼知識。瀏覽器
計算機最開始只支持ASCII碼,一個字符用一個字節表示,只用了低7位,最高位爲0,所以總共有128個ASCII碼,範圍爲0~127。後來爲了支持多種地區的語言,各大組織機構和IT廠商開始發明它們本身的編碼方案,以便彌補ASCII編碼的不足,如GB2312編碼、GBK編碼和Big5編碼等。但這些編碼都只是針對局部地區或少數語言文字,沒有辦法表達全部的語言文字。並且這些不一樣的編碼之間並無任何聯繫,它們之間的轉換須要經過查表來實現。測試
爲了提升計算機的信息處理和交換功能,使得世界各國的文字都能在計算機中處理,從1984年起,ISO組織就開始研究制定一個全新的標準:通用多八位(即多字節)編碼字符集(Universal Multiple-Octet Coded Character Set),簡稱UCS。標準的編號爲:ISO 10646。這一標準爲世界各類主要語言的字符(包括簡體及繁體的中文字)及附加符號,編制統一的內碼。this
統一碼(Unicode)是Universal Code的縮寫,是由另外一個叫「Unicode學術學會」(The Unicode Consortium)的機構制定的字符編碼系統。Unicode與ISO 10646國際編碼標準從內容上來講是同步一致的。具體可參考:Unicode 。編碼
ANSI不表明具體的編碼,它是指本地編碼。好比在簡體版windows上它表示GB2312編碼,在繁體版windows上它表示Big5編碼,在日文操做系統上它表示JIS編碼。因此若是您新建了個文本文件並保存爲ANSI編碼,那麼您如今應該知道這個文件的編碼爲本地編碼。spa
Unicode編碼是和字符表一一映射的。好比56DE表明漢字'回',這種映射關係是固定不變的。通俗的說Unicode編碼就是字符表的座標,經過56DE就能找到漢字'回'。Unicode編碼的實現包括UTF八、UTF1六、UTF32等等。操作系統
Unicode自己定義的就是每一個字符的數值,是字符和天然數的映射關係,而UTF-8或者UTF-16甚至UTF-32則定義瞭如何在字節流中斷字,是計算機領域的概念。.net
經過上圖咱們知道,UTF-8編碼爲變長的編碼方式,佔1~6個字節,可經過Unicode編碼值的區間來判斷,而且每一個組成UTF8字符的字節都是有規律可循的。本文只討論UTF8和UTF16這兩種編碼。
UTF16編碼使用固定的2個字節來存儲。由於是多字節存儲,因此它的存儲方式分爲2種:大端序和小端序。UTF16編碼是Unicode最直接的實現方式,一般咱們在windows上新建文本文件後保存爲Unicode編碼,其實就是保存爲UTF16編碼。UTF16編碼在windows上採用小端序的方式存儲,如下我新建了個文本文件並保存爲Unicode編碼來測試,文件中只輸入了一個漢字'回',以後我用Editplus打開它,切換到十六進制方式查看,如圖所示:
咱們看到有4個字節,前2個字節FF FE是文件頭,表示這是一個UTF16編碼的文件,而DE 56則是'回'的UTF16編碼的十六進制。咱們常用的JavaScript語言,它內部就是採用UTF16編碼,而且它的存儲方式爲大端序,來看一個例子:
1 |
<script type= "text/javascript" > |
2 |
console.group( 'Test Unicode: ' ); |
3 |
console.log(( '回' .charCodeAt(0)).toString(16).toUpperCase()); |
4 |
</script> |
很明顯跟剛纔Editplus顯示的不同,順序是相反的,這是由於字節序不同。具體可參考:UTF-16 。
UTF8是採用變長的編碼方式,爲1~6個字節,但一般咱們只把它看做單字節或三字節的實現,由於其它狀況實在少見。UTF8編碼經過多個字節組合的方式來顯示,這是計算機處理UTF8的機制,它是無字節序之分的,而且每一個字節都很是有規律,詳見上圖,在此再也不詳述。
UTF16和UTF8之間的相互轉換能夠經過上圖的轉換表來實現,判斷Unicode碼所在的區間就能夠獲得這個字符是由幾個字節所組成,以後經過移位來實現。咱們用漢字'回'來舉一個轉換的例子。
咱們已經知道漢字'回'的Unicode碼是0x56DE,它介於U+00000800 – U+0000FFFF之間,因此它是用三個字節來表示的。
因此咱們須要將0x56DE這個雙字節的值變爲三字節的值,注意上圖中的x部分,就是對應0x56DE的各位字節,若是您數一下x的個數,會發現恰好是16位。
從0x56DE中取出4位放在低位,並和二進制的1110結合,這就是第一個字節。從0x56DE中剩下的字節中取出6位放在低位,並和二進制的10結合,這就是第二個字節。第三個字節依照相似的方式實現。
爲了讓你們更好的理解,如下代碼只是實現漢字'回'的轉換,代碼以下:
01 |
<script type= "text/javascript" > |
02 |
/** |
03 |
* 轉換對照表 |
04 |
* U+00000000 – U+0000007F 0xxxxxxx |
05 |
* U+00000080 – U+000007FF 110xxxxx 10xxxxxx |
06 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
07 |
* U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
08 |
* U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
09 |
* U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
10 |
*/ |
11 |
/* |
12 |
* '回'的Unicode編碼爲:0x56DE,它介於U+00000800 – U+0000FFFF之間,因此它佔用三個字節。 |
13 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
14 |
*/ |
15 |
var ucode = 0x56DE; |
16 |
// 1110xxxx |
17 |
var byte1 = 0xE0 | ((ucode >> 12) & 0x0F); |
18 |
// 10xxxxxx |
19 |
var byte2 = 0x80 | ((ucode >> 6) & 0x3F); |
20 |
// 10xxxxxx |
21 |
var byte3 = 0x80 | (ucode & 0x3F); |
22 |
var utf8 = String.fromCharCode(byte1) |
23 |
+ String.fromCharCode(byte2) |
24 |
+ String.fromCharCode(byte3); |
25 |
26 |
console.group( 'Test UTF16ToUTF8: ' ); |
27 |
console.log(utf8); |
28 |
console.groupEnd(); |
29 |
</script> |
輸出的結果看起來像亂碼,這是由於JavaScript不知道如何顯示UTF8的字符。您或許會說輸出不正常轉換還有什麼用,但您應該知道轉換的目的還常常用於傳輸或API的須要。
這是UTF16轉換到UTF8的逆轉換,一樣須要對照轉換表來實現。仍是接上一個例子,咱們已經獲得了漢字'回'的UTF8編碼,這是三個字節的,咱們只須要按照轉換表來轉成雙字節的,如圖所示,咱們須要保留下全部的x。
代碼以下:
01 |
<script type= "text/javascript" > |
02 |
/** |
03 |
* 轉換對照表 |
04 |
* U+00000000 – U+0000007F 0xxxxxxx |
05 |
* U+00000080 – U+000007FF 110xxxxx 10xxxxxx |
06 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
07 |
* U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
08 |
* U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
09 |
* U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
10 |
*/ |
11 |
/* |
12 |
* '回'的Unicode編碼爲:0x56DE,它介於U+00000800 – U+0000FFFF之間,因此它佔用三個字節。 |
13 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
14 |
*/ |
15 |
var ucode = 0x56DE; |
16 |
// 1110xxxx |
17 |
var byte1 = 0xE0 | ((ucode >> 12) & 0x0F); |
18 |
// 10xxxxxx |
19 |
var byte2 = 0x80 | ((ucode >> 6) & 0x3F); |
20 |
// 10xxxxxx |
21 |
var byte3 = 0x80 | (ucode & 0x3F); |
22 |
var utf8 = String.fromCharCode(byte1) |
23 |
+ String.fromCharCode(byte2) |
24 |
+ String.fromCharCode(byte3); |
25 |
26 |
console.group( 'Test UTF16ToUTF8: ' ); |
27 |
console.log(utf8); |
28 |
console.groupEnd(); |
29 |
/** ------------------------------------------------------------------------------------*/ |
30 |
// 由三個字節組成,因此分別取出 |
31 |
var c1 = utf8.charCodeAt(0); |
32 |
var c2 = utf8.charCodeAt(1); |
33 |
var c3 = utf8.charCodeAt(2); |
34 |
/* |
35 |
* 須要經過判斷特定位的方式來轉換,但這裏是已知是三個字節,因此忽略判斷,而是直接拿到全部的x,組成16位。 |
36 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
37 |
*/ |
38 |
// 丟棄第一個字節的高四位並和第二個字節的高四位組成一個字節 |
39 |
var b1 = (c1 << 4) | ((c2 >> 2) & 0x0F); |
40 |
// 同理第二個字節和第三個字節組合 |
41 |
var b2 = ((c2 & 0x03) << 6) | (c3 & 0x3F); |
42 |
// 將b1和b2組成16位 |
43 |
var ucode = ((b1 & 0x00FF) << 8) | b2; |
44 |
console.group( 'Test UTF8ToUTF16: ' ); |
45 |
console.log(ucode.toString(16).toUpperCase(), String.fromCharCode(ucode)); |
46 |
console.groupEnd(); |
47 |
</script> |
知道了轉換規則,就很容易實現了。
Base64編碼要求把3個8位字節(3*8=24)轉化爲4個6位的字節(4*6=24),以後在6位的前面補兩個0,造成8位一個字節的形式。因爲2的6次方爲64,因此每6個位爲一個單元,對應某個可打印字符。當原數據不是3的整數倍時,若是最後剩下兩個輸入數據,在編碼結果後加1個「=;若是最後剩下一個輸入數據,編碼結果後加2個「=;若是沒有剩下任何數據,就什麼都不要加,這樣才能夠保證資料還原的正確性。
每6個單元高位補2個零造成的字節位於0~63之間,經過在轉碼錶中查找對應的可打印字符。「=」用於填充。以下圖所示爲轉碼錶。
具體可參考: Base64 。
解碼是編碼的逆過程,先看後面補了幾個「=」號,最多隻可能補2個「=」號。一個「=」至關於補了2個0,因此去掉後面補的0後,再按8位展開,便可還原。
以前已經詳細講解了整個過程,本文的例子都是採用UTF8編碼的字符串做爲Base64編碼的基礎。由於JavaScript內部是使用Unicode編碼,因此須要有個轉換過程,原理以前也詳細講解過並給出了示例,如下是代碼實現:
001 |
<script type= "text/javascript" > |
002 |
/** |
003 |
* UTF16和UTF8轉換對照表 |
004 |
* U+00000000 – U+0000007F 0xxxxxxx |
005 |
* U+00000080 – U+000007FF 110xxxxx 10xxxxxx |
006 |
* U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
007 |
* U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
008 |
* U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
009 |
* U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
010 |
*/ |
011 |
var Base64 = { |
012 |
// 轉碼錶 |
013 |
table : [ |
014 |
'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , |
015 |
'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , |
016 |
'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , |
017 |
'Y' , 'Z' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , |
018 |
'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , |
019 |
'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , |
020 |
'w' , 'x' , 'y' , 'z' , '0' , '1' , '2' , '3' , |
021 |
'4' , '5' , '6' , '7' , '8' , '9' , '+' , '/' |
022 |
], |
023 |
UTF16ToUTF8 : function (str) { |
024 |
var res = [], len = str.length; |
025 |
for ( var i = 0; i < len; i++) { |
026 |
var code = str.charCodeAt(i); |
027 |
if (code > 0x0000 && code <= 0x007F) { |
028 |
// 單字節,這裏並不考慮0x0000,由於它是空字節 |
029 |
// U+00000000 – U+0000007F 0xxxxxxx |
030 |
res.push(str.charAt(i)); |
031 |
} else if (code >= 0x0080 && code <= 0x07FF) { |
032 |
// 雙字節 |
033 |
// U+00000080 – U+000007FF 110xxxxx 10xxxxxx |
034 |
// 110xxxxx |
035 |
var byte1 = 0xC0 | ((code >> 6) & 0x1F); |
036 |
// 10xxxxxx |
037 |
var byte2 = 0x80 | (code & 0x3F); |
038 |
res.push( |
039 |
String.fromCharCode(byte1), |
040 |
String.fromCharCode(byte2) |
041 |
); |
042 |
} else if (code >= 0x0800 && code <= 0xFFFF) { |
043 |
// 三字節 |
044 |
// U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx |
045 |
// 1110xxxx |
046 |
var byte1 = 0xE0 | ((code >> 12) & 0x0F); |
047 |
// 10xxxxxx |
048 |
var byte2 = 0x80 | ((code >> 6) & 0x3F); |
049 |
// 10xxxxxx |
050 |
var byte3 = 0x80 | (code & 0x3F); |
051 |
res.push( |
052 |
String.fromCharCode(byte1), |
053 |
String.fromCharCode(byte2), |
054 |
String.fromCharCode(byte3) |
055 |
); |
056 |
} else if (code >= 0x00010000 && code <= 0x001FFFFF) { |
057 |
// 四字節 |
058 |
// U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
059 |
} else if (code >= 0x00200000 && code <= 0x03FFFFFF) { |
060 |
// 五字節 |
061 |
// U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
062 |
} else /** if (code >= 0x04000000 && code <= 0x7FFFFFFF)*/ { |
063 |
// 六字節 |
064 |
// U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
065 |
} |
066 |
} |
067 |
068 |
return res.join( '' ); |
069 |
}, |
070 |
UTF8ToUTF16 : function (str) { |
071 |
var res = [], len = str.length; |
072 |
var i = 0; |
073 |
for ( var i = 0; i < len; i++) { |
074 |
var code = str.charCodeAt(i); |
075 |
// 對第一個字節進行判斷 |
076 |
if (((code >> 7) & 0xFF) == 0x0) { |
077 |
// 單字節 |
078 |
// 0xxxxxxx |
079 |
res.push(str.charAt(i)); |
080 |
} else if (((code >> 5) & 0xFF) == 0x6) { |
081 |
// 雙字節 |
082 |
// 110xxxxx 10xxxxxx |
083 |
var code2 = str.charCodeAt(++i); |
084 |
var byte1 = (code & 0x1F) << 6; |
085 |
var byte2 = code2 & 0x3F; |
086 |
var utf16 = byte1 | byte2; |
087 |
res.push(Sting.fromCharCode(utf16)); |
088 |
} else if (((code >> 4) & 0xFF) == 0xE) { |
089 |
// 三字節 |
090 |
// 1110xxxx 10xxxxxx 10xxxxxx |
091 |
var code2 = str.charCodeAt(++i); |
092 |
var code3 = str.charCodeAt(++i); |
093 |
var byte1 = (code << 4) | ((code2 >> 2) & 0x0F); |
094 |
var byte2 = ((code2 & 0x03) << 6) | (code3 & 0x3F); |
095 |
var utf16 = ((byte1 & 0x00FF) << 8) | byte2 |
096 |
res.push(String.fromCharCode(utf16)); |
097 |
} else if (((code >> 3) & 0xFF) == 0x1E) { |
098 |
// 四字節 |
099 |
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
100 |
} else if (((code >> 2) & 0xFF) == 0x3E) { |
101 |
// 五字節 |
102 |
// 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
103 |
} else /** if (((code >> 1) & 0xFF) == 0x7E)*/ { |
104 |
// 六字節 |
105 |
// 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
106 |
} |
107 |
} |
108 |
109 |
return res.join( '' ); |
110 |
}, |
111 |
encode : function (str) { |
112 |
if (!str) { |
113 |
return '' ; |
114 |
} |
115 |
var utf8 = this .UTF16ToUTF8(str); // 轉成UTF8 |
116 |
var i = 0; // 遍歷索引 |
117 |
var len = utf8.length; |
118 |
var res = []; |
119 |
while (i < len) { |
120 |
var c1 = utf8.charCodeAt(i++) & 0xFF; |
121 |
res.push( this .table[c1 >> 2]); |
122 |
// 須要補2個= |
123 |
if (i == len) { |
124 |
res.push( this .table[(c1 & 0x3) << 4]); |
125 |
res.push( '==' ); |
126 |
break ; |
127 |
} |
128 |
var c2 = utf8.charCodeAt(i++); |
129 |
// 須要補1個= |
130 |
if (i == len) { |
131 |
res.push( this .table[((c1 & 0x3) << 4) | ((c2 >> 4) & 0x0F)]); |
132 |
res.push( this .table[(c2 & 0x0F) << 2]); |
133 |
res.push( '=' ); |
134 |
break ; |
135 |
} |
136 |
var c3 = utf8.charCodeAt(i++); |
137 |
res.push( this .table[((c1 & 0x3) << 4) | ((c2 >> 4) & 0x0F)]); |
138 |
res.push( this .table[((c2 & 0x0F) << 2) | ((c3 & 0xC0) >> 6)]); |
139 |
res.push( this .table[c3 & 0x3F]); |
140 |
} |
141 |
142 |
return res.join( '' ); |
143 |
}, |
144 |
decode : function (str) { |
145 |
if (!str) { |
146 |
return '' ; |
147 |
} |
148 |
149 |
var len = str.length; |
150 |
var i = 0; |
151 |
var res = []; |
152 |
153 |
while (i < len) { |
154 |
code1 = this .table.indexOf(str.charAt(i++)); |
155 |
code2 = this .table.indexOf(str.charAt(i++)); |
156 |
code3 = this .table.indexOf(str.charAt(i++)); |
157 |
code4 = this .table.indexOf(str.charAt(i++)); |
158 |
159 |
c1 = (code1 << 2) | (code2 >> 4); |
160 |
c2 = ((code2 & 0xF) << 4) | (code3 >> 2); |
161 |
c3 = ((code3 & 0x3) << 6) | code4; |
162 |
163 |
res.push(String.fromCharCode(c1)); |
164 |
165 |
if (code3 != 64) { |
166 |
res.push(String.fromCharCode(c2)); |
167 |
} |
168 |
if (code4 != 64) { |
169 |
res.push(String.fromCharCode(c3)); |
170 |
} |
171 |
172 |
} |
173 |
174 |
return this .UTF8ToUTF16(res.join( '' )); |
175 |
} |
176 |
}; |
177 |
178 |
console.group( 'Test Base64: ' ); |
179 |
var b64 = Base64.encode( 'Hello, oschina!又是一年春來到~' ); |
180 |
console.log(b64); |
181 |
console.log(Base64.decode(b64)); |
182 |
console.groupEnd(); |
183 |
</script> |
不得不說,在JavaScript中實現確實很麻煩。咱們來看下PHP對一樣的字符串編碼的結果:
由於字符編碼是同樣的,因此結果也同樣。