文章大綱:
1.為什么要編碼?
2.各種編碼集介紹
3.UTF-8編碼規(guī)則介紹
4.編碼所涉及場(chǎng)景
5.相關(guān)筆試題答案和分析
為什么要編碼?
由于人類(lèi)的語(yǔ)言有太多称杨,因而表示這些語(yǔ)言的符號(hào)太多,無(wú)法用計(jì)算機(jī)中一個(gè)基本的存儲(chǔ)單元—— byte 來(lái)表示,因此必須要經(jīng)過(guò)拆分或一些翻譯工作矫限,才能讓計(jì)算機(jī)能理解。所以編碼的使命也就來(lái)了佩抹,讓全球的人民可以互相溝通叼风。這和國(guó)際通用語(yǔ)言英語(yǔ)大概是一個(gè)道理,總要有一個(gè)大家互相溝通的渠道棍苹。編碼的規(guī)則有很多種无宿,下面就一一介紹一下各種編碼集
各種編碼集介紹
- ASCII 碼
學(xué)過(guò)計(jì)算機(jī)的人都知道 ASCII 碼,總共有 128 個(gè)廊勃,用一個(gè)字節(jié)的低 7 位表示懈贺,0~31 是控制字符如換行回車(chē)刪除等;32~126 是打印字符坡垫,可以通過(guò)鍵盤(pán)輸入并且能夠顯示出來(lái)梭灿。 - ISO-8859-1
128 個(gè)字符顯然是不夠用的,于是 ISO 組織在 ASCII 碼基礎(chǔ)上又制定了一些列標(biāo)準(zhǔn)用來(lái)擴(kuò)展 ASCII 編碼冰悠,它們是 ISO-8859-1~ISO-8859-15堡妒,其中 ISO-8859-1 涵蓋了大多數(shù)西歐語(yǔ)言字符,所有應(yīng)用的最廣泛溉卓。ISO-8859-1 仍然是單字節(jié)編碼皮迟,它總共能表示 256 個(gè)字符搬泥。 - GB2312
它的全稱(chēng)是《信息交換用漢字編碼字符集 基本集》,它是雙字節(jié)編碼伏尼, 第一個(gè)字節(jié)稱(chēng)為“高位字節(jié)”忿檩,第二個(gè)字節(jié)稱(chēng)為“低位字節(jié)”。 “高位字節(jié)”使用了0xA1-0xF7(把01-87區(qū)的區(qū)號(hào)加上0xA0)爆阶,“低位字節(jié)”使用了0xA1-0xFE(把01-94加上0xA0)燥透。
例如 “啊”字在大多數(shù)程序中,會(huì)以0xB0A1儲(chǔ)存(與區(qū)位碼對(duì)比:0xB0=0xA0+16,0xA1=0xA0+1)辨图。
其中從 A1-A9 是符號(hào)區(qū)班套,總共包含 682 個(gè)符號(hào),
從 B0-F7 是漢字區(qū)故河,包含 6763 個(gè)漢字吱韭。
- GBK
全稱(chēng)叫《漢字內(nèi)碼擴(kuò)展規(guī)范》,是國(guó)家技術(shù)監(jiān)督局為 windows95 所制定的新的漢字內(nèi)碼規(guī)范鱼的,它的出現(xiàn)是為了擴(kuò)展 GB2312理盆,加入更多的漢字,它的編碼范圍是 8140~FEFE(去掉 XX7F)總共有 23940 個(gè)碼位鸳吸,它能表示 21003 個(gè)漢字熏挎,它的編碼是和 GB2312 兼容的速勇,也就是說(shuō)用 GB2312 編碼的漢字可以用 GBK 來(lái)解碼晌砾,并且不會(huì)有亂碼。 - GB18030
全稱(chēng)是《信息交換用漢字編碼字符集》烦磁,是我國(guó)的強(qiáng)制標(biāo)準(zhǔn)养匈,它可能是單字節(jié)、雙字節(jié)或者四字節(jié)編碼都伪,它的編碼與 GB2312 編碼兼容呕乎,這個(gè)雖然是國(guó)家標(biāo)準(zhǔn),但是實(shí)際應(yīng)用系統(tǒng)中使用的并不廣泛陨晶。 - UTF-16
說(shuō)到 UTF 必須要提到 Unicode(Universal Code 統(tǒng)一碼)猬仁,ISO 試圖想創(chuàng)建一個(gè)全新的超語(yǔ)言字典,世界上所有的語(yǔ)言都可以通過(guò)這本字典來(lái)相互翻譯先誉∈簦可想而知這個(gè)字典是多么的復(fù)雜,關(guān)于 Unicode 的詳細(xì)規(guī)范可以參考相應(yīng)文檔褐耳。Unicode 是 Java 和 XML 的基礎(chǔ)诈闺,下面詳細(xì)介紹 Unicode 在計(jì)算機(jī)中的存儲(chǔ)形式。
UTF-16 具體定義了 Unicode 字符在計(jì)算機(jī)中存取方法铃芦。UTF-16 用兩個(gè)字節(jié)來(lái)表示 Unicode 轉(zhuǎn)化格式雅镊,這個(gè)是定長(zhǎng)的表示方法襟雷,不論什么字符都可以用兩個(gè)字節(jié)表示,兩個(gè)字節(jié)是 16 個(gè) bit仁烹,所以叫 UTF-16耸弄。UTF-16 表示字符非常方便,每?jī)蓚€(gè)字節(jié)表示一個(gè)字符卓缰,這個(gè)在字符串操作時(shí)就大大簡(jiǎn)化了操作叙赚,這也是 Java 以 UTF-16 作為內(nèi)存的字符存儲(chǔ)格式的一個(gè)很重要的原因。 - UTF-8
UTF-16 統(tǒng)一采用兩個(gè)字節(jié)表示一個(gè)字符僚饭,雖然在表示上非常簡(jiǎn)單方便震叮,但是也有其缺點(diǎn),有很大一部分字符用一個(gè)字節(jié)就可以表示的現(xiàn)在要兩個(gè)字節(jié)表示鳍鸵,存儲(chǔ)空間放大了一倍苇瓣,在現(xiàn)在的網(wǎng)絡(luò)帶寬還非常有限的今天,這樣會(huì)增大網(wǎng)絡(luò)傳輸?shù)牧髁砍ス裕乙矝](méi)必要击罪。而 UTF-8 采用了一種變長(zhǎng)技術(shù),每個(gè)編碼區(qū)域有不同的字碼長(zhǎng)度贪薪。不同類(lèi)型的字符可以是由 1~6 個(gè)字節(jié)組成媳禁。
UTF-8 有以下編碼規(guī)則:
如果一個(gè)字節(jié),最高位(第 8 位)為 0画切,表示這是一個(gè) ASCII 字符(00 - 7F)竣稽。可見(jiàn)霍弹,所有 ASCII 編碼已經(jīng)是 UTF-8 了毫别。
如果一個(gè)字節(jié),以 11 開(kāi)頭典格,連續(xù)的 1 的個(gè)數(shù)暗示這個(gè)字符的字節(jié)數(shù)岛宦,例如:110xxxxx 代表它是雙字節(jié) UTF-8 字符的首字節(jié)。
如果一個(gè)字節(jié)耍缴,以 10 開(kāi)始砾肺,表示它不是首字節(jié),需要向前查找才能得到當(dāng)前字符的首字節(jié)
UTF-8編碼規(guī)則介紹
如下圖所示防嗡,這里UTF-8可變長(zhǎng)編碼用到了一個(gè)小技巧:用幾位冗余信息告訴系統(tǒng)变汪,當(dāng)前字符有沒(méi)有結(jié)束,是不是還需要繼續(xù)往下讀下一個(gè)字節(jié)本鸣。
下圖演示了字符串“I am 君山”用 UTF-8 編碼的結(jié)果:
君 = 541b = 0101 0100 0001 1011 (Unicode)
我們把君的unicode從后往前數(shù)疫衩,每6個(gè)為一個(gè)字節(jié)(因?yàn)榍?位固定為10),然后計(jì)算一下
需要用3個(gè)字節(jié)編碼,把0101010000011011切成3部分變成:
0101 010000 011011
分別套上UTF-8字符頭:
1110 0101 10 010000 10 011011 = e5 90 9b
編碼所涉及場(chǎng)景
- URL 的編解碼
用戶提交一個(gè) URL闷煤,這個(gè) URL 中可能存在中文童芹,因此需要編碼,如何對(duì)這個(gè) URL 進(jìn)行編碼鲤拿?根據(jù)什么規(guī)則來(lái)編碼假褪?有如何來(lái)解碼?如下圖一個(gè) URL:
上圖中 PathInfo 和 QueryString 出現(xiàn)了中文近顷,當(dāng)我們?cè)跒g覽器中直接輸入這個(gè) URL 時(shí)生音,在瀏覽器端和服務(wù)端會(huì)如何編碼和解析這個(gè) URL 呢?為了驗(yàn)證瀏覽器是怎么編碼 URL 的我們選擇 FireFox 瀏覽器并通過(guò) HTTPFox 插件觀察我們請(qǐng)求的 URL 的實(shí)際的內(nèi)容窒升,以下是 URL:HTTP://localhost:8080/examples/servlets/servlet/ 君山 ?author= 君山在中文 FireFox3.6.12 的測(cè)試結(jié)果
君山的編碼結(jié)果分別是:e5 90 9b e5 b1 b1缀遍,be fd c9 bd,查閱上一屆的編碼可知饱须,PathInfo 是 UTF-8 編碼而 QueryString 是經(jīng)過(guò) GBK 編碼域醇,至于為什么會(huì)有“%”?查閱 URL 的編碼規(guī)范 RFC3986 可知瀏覽器編碼 URL 是將非 ASCII 字符按照某種編碼格式編碼成 16 進(jìn)制數(shù)字然后將每個(gè) 16 進(jìn)制表示的字節(jié)前加上“%”蓉媳,所以最終的 URL 就成了上圖的格式了譬挚。
默認(rèn)情況下中文 IE 最終的編碼結(jié)果也是一樣的,不過(guò) IE 瀏覽器可以修改 URL 的編碼格式在選項(xiàng) -> 高級(jí) -> 國(guó)際里面的發(fā)送 UTF-8 URL 選項(xiàng)可以取消酪呻。
從上面測(cè)試結(jié)果可知瀏覽器對(duì) PathInfo 和 QueryString 的編碼是不一樣的减宣,不同瀏覽器對(duì) PathInfo 也可能不一樣,這就對(duì)服務(wù)器的解碼造成很大的困難玩荠,下面我們以 Tomcat 為例看一下漆腌,Tomcat 接受到這個(gè) URL 是如何解碼的。
解析請(qǐng)求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中姨蟋,這個(gè)方法把傳過(guò)來(lái)的 URL 的 byte[] 設(shè)置到 org.apache.coyote.Request 的相應(yīng)的屬性中屉凯。這里的 URL 仍然是 byte 格式,轉(zhuǎn)成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的
protected void convertURI(MessageBytes uri, Request request)
throws Exception {
ByteChunk bc = uri.getByteChunk();
int length = bc.getLength();
CharChunk cc = uri.getCharChunk();
cc.allocate(length, -1);
String enc = connector.getURIEncoding();
if (enc != null) {
B2CConverter conv = request.getURIConverter();
try {
if (conv == null) {
conv = new B2CConverter(enc);
request.setURIConverter(conv);
}
} catch (IOException e) {...}
if (conv != null) {
try {
conv.convert(bc, cc, cc.getBuffer().length -
cc.getEnd());
uri.setChars(cc.getBuffer(), cc.getStart(),
cc.getLength());
return;
} catch (IOException e) {...}
}
}
// Default encoding: fast conversion
byte[] bbuf = bc.getBuffer();
char[] cbuf = cc.getBuffer();
int start = bc.getStart();
for (int i = 0; i < length; i++) {
cbuf[i] = (char) (bbuf[i + start] & 0xff);
}
uri.setChars(cbuf, 0, length);
}
從上面的代碼中可以知道對(duì) URL 的 URI 部分進(jìn)行解碼的字符集是在 connector 的 <Connector URIEncoding=”UTF-8”/> 中定義的眼溶,如果沒(méi)有定義,那么將以默認(rèn)編碼 ISO-8859-1 解析晓勇。所以如果有中文 URL 時(shí)最好把 URIEncoding 設(shè)置成 UTF-8 編碼堂飞。
QueryString 又如何解析? GET 方式 HTTP 請(qǐng)求的 QueryString 與 POST 方式 HTTP 請(qǐng)求的表單參數(shù)都是作為 Parameters 保存绑咱,都是通過(guò) request.getParameter 獲取參數(shù)值绰筛。對(duì)它們的解碼是在 request.getParameter 方法第一次被調(diào)用時(shí)進(jìn)行的。request.getParameter 方法被調(diào)用時(shí)將會(huì)調(diào)用 org.apache.catalina.connector.Request 的 parseParameters 方法描融。這個(gè)方法將會(huì)對(duì) GET 和 POST 方式傳遞的參數(shù)進(jìn)行解碼铝噩,但是它們的解碼字符集有可能不一樣。QueryString 的解碼字符集是在哪定義的呢窿克?它本身是通過(guò) HTTP 的 Header 傳到服務(wù)端的骏庸,并且也在 URL 中毛甲,是否和 URI 的解碼字符集一樣呢?從前面瀏覽器對(duì) PathInfo 和 QueryString 的編碼采取不同的編碼格式不同可以猜測(cè)到解碼字符集肯定也不會(huì)是一致的具被。的確是這樣 QueryString 的解碼字符集要么是 Header 中 ContentType 中定義的 Charset 要么就是默認(rèn)的 ISO-8859-1玻募,要使用 ContentType 中定義的編碼就要設(shè)置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/> 中的 useBodyEncodingForURI 設(shè)置為 true。這個(gè)配置項(xiàng)的名字有點(diǎn)讓人產(chǎn)生混淆一姿,它并不是對(duì)整個(gè) URI 都采用 BodyEncoding 進(jìn)行解碼而僅僅是對(duì) QueryString 使用 BodyEncoding 解碼七咧,這一點(diǎn)還要特別注意。
從上面的 URL 編碼和解碼過(guò)程來(lái)看叮叹,比較復(fù)雜艾栋,而且編碼和解碼并不是我們?cè)趹?yīng)用程序中能完全控制的,所以在我們的應(yīng)用程序中應(yīng)該盡量避免在 URL 中使用非 ASCII 字符蛉顽,不然很可能會(huì)碰到亂碼問(wèn)題裹粤,當(dāng)然在我們的服務(wù)器端最好設(shè)置 <Connector/> 中的 URIEncoding 和 useBodyEncodingForURI 兩個(gè)參數(shù)。
還有其他的比如:HTTP Header 的編解碼蜂林、POST 表單的編解碼遥诉、HTTP BODY 的編解碼等請(qǐng)直接去這篇文章里面看深入分析 Java 中的中文編碼問(wèn)題。就不在這里重復(fù)了
相關(guān)筆試題答案和分析
1.URLEncoding是一種應(yīng)用于HTTP協(xié)議的編碼方式噪叙,字符串“你好”基于UTF-8的URLEncoding編碼為: “%E4%BD%A0%E5%A5%BD”
其中E4矮锈、BD、A0為字符“你”的UTF-8編碼的十六進(jìn)制形式(3個(gè)字節(jié))睁蕾,而E5苞笨、A5、BD為字符“好”的UTF-8編碼的十六進(jìn)制形式子眶。
下面的代碼用程序的方式輸出字符串“你好”的基于UTF-8的URLEncoding序列:
String msg = "你好";
空白處1
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bs.length; i++) {
空白處2
sb.append("%").append(str);
}
System.out.println(sb.toString());
空白處1及空白處2分別應(yīng)填入的代碼是()瀑凝。
A. byte[] bs = msg.getChars("utf-8");
和 String str = Integer.toHexString(bs[i]& 0xff).toUpperCase();
B. byte[] bs = msg.getBytes("utf-8");
和 String str = Integer.toHexString(bs[i]).toUpperCase();
C. byte[] bs = msg.getBytes("utf-8");
和 String str = Integer.toHexString(bs[i] & 0xff).toUpperCase();
D. byte[] bs = msg.getBytes();
和 String str = Integer.toHexString(bs[i]).toUpperCase();
找了很久,實(shí)在沒(méi)找到太多相關(guān)的題目臭杰。如果大家有題目補(bǔ)充或者鏈接粤咪,歡迎告知。我把題目加入進(jìn)來(lái)渴杆。
答案解析:首先String的getChatrs方法寥枝,不只一個(gè)參數(shù)。我在IDE里面只加入一個(gè)參數(shù)磁奖,就像選項(xiàng)中的A這樣囊拜。是會(huì)報(bào)錯(cuò)的。由此可知比搭,答案就在BCD中的一個(gè)冠跷。我們?cè)倏纯碊和BC的區(qū)別,在getBytes()方法上,如果你不傳參數(shù)蜜托,它默認(rèn)的是使用操作系統(tǒng)默認(rèn)的編碼格式抄囚。那么這樣就有可能會(huì)出錯(cuò),在不同的默認(rèn)系統(tǒng)環(huán)境下盗冷。所以指定了編碼utf-8的是最保險(xiǎn)的怠苔。那么我們看BC的答案,差別就在于有沒(méi)有&0xff仪糖。那我們輸出一下相應(yīng)的結(jié)果:
我們可以看到柑司,輸出的是32位(int)的二進(jìn)制,然后前面都是符號(hào)位的1的補(bǔ)充锅劝,其實(shí)真正有用的只有最后8位攒驰。我們看到最終答案中只要去掉前面的6個(gè)F就是我們要的答案了,所以&0xff的原因也就是如此故爵。0xff(32位二進(jìn)制)=24個(gè)0 1111 1111 這樣就保證了前24位為0玻粪,后8位不變。
參考文獻(xiàn):
通過(guò)故事來(lái)串連各種編碼集:Unicode 和 UTF-8 有何區(qū)別诬垂?
深入分析 Java 中的中文編碼問(wèn)題
Java 中字節(jié)流與字符流的區(qū)別?
字符編碼筆記:ASCII劲室,Unicode和UTF-8
在本文中的使用位置:第二段中講解GB2312部分:GB2312區(qū)位碼、編碼表與編碼規(guī)則