從瀏覽器到服務(wù)端的中文亂碼深入分析
概述
前段時(shí)間陸陸續(xù)續(xù)有一些同事跟我詢問中文亂碼問題,每個(gè)人的問題也都大同小異。而我最早之前也一直想寫一篇這樣的文章,無奈都騰不出富裕的時(shí)間,或者說拖延癥比較嚴(yán)重(其實(shí)還是懶),這次就索性對(duì)自己狠一把,對(duì)這個(gè)問題做一個(gè)總結(jié)。
我們知道http協(xié)議是請(qǐng)求-響應(yīng)式的,平常出現(xiàn)的亂碼問題也就都隱藏在這一問一答之中,如果能明白字符在這個(gè)期間所走的鏈路,以及在這個(gè)鏈路中都經(jīng)歷了怎樣的字符轉(zhuǎn)換,那么遇到任何煩人的亂碼問題也能夠迎刃而解。
下面我會(huì)根據(jù)自身工作中的經(jīng)歷,講述基于http協(xié)議在開發(fā)過程中遇到的字符亂碼問題。
響應(yīng)(response)時(shí)遇到的亂碼問題
兩千多年前孔子看見顏回煮飯時(shí)先偷偷吃了一些,便用言語(yǔ)責(zé)怪了顏回。顏回解釋并沒有偷吃,是有臟東西掉進(jìn)鍋里了,他把有臟東西的飯撈出來吃掉了。后來孔子感慨,所信者目也,而目猶不可信。
當(dāng)你在瀏覽器里看到響應(yīng)內(nèi)容是亂碼時(shí),你會(huì)認(rèn)為一定是程序吐出的字符就是亂碼,解決這個(gè)問題的辦法就是修改程序。事實(shí)真的是這樣的嗎?為了說明這個(gè)問題,我寫了一段簡(jiǎn)單的程序用來模擬web程序,這段程序的作用就是輸出utf-8編碼的“中國(guó)”兩個(gè)字符。下面我們用火狐和chrome訪問這個(gè)程序。
用火狐訪問http://localhost:8080
用chrome訪問http://localhost:8080
從上面可以看到,對(duì)于相同的輸出,不同的瀏覽器展現(xiàn)了不同的結(jié)果。Firefox在瀏覽器正文顯示的是亂碼,而在下面的“響應(yīng)”標(biāo)簽中顯示了正確的字符。Chrome則跟Firefox相反,正文顯示正確,標(biāo)簽”response”顯示亂碼。并且兩個(gè)瀏覽器顯示的亂碼也是不一致的, firefox顯示成了三個(gè)字符,chrome則顯示成六個(gè)字符。
上面說過,我的這段web程序是將“中國(guó)”這兩個(gè)字符按照utf-8編碼輸出的,
難道是在輸出的過程中被轉(zhuǎn)換成了別的編碼?為了一探究竟我需要看到程序輸出的原始字節(jié)碼,原始字節(jié)碼用firefox和chrome自帶的工具是看不到的,這里我用wireshak分別對(duì)兩個(gè)兩個(gè)瀏覽器做了抓包。
“中國(guó)”這兩個(gè)字符在utf-8編碼中對(duì)應(yīng)的編碼為e4b8ad(中)、e59bbd(國(guó)),如果我們抓到的包中也看到的是這六個(gè)字節(jié),那就說明程序的輸出是沒有問題。
對(duì)firefox的抓包:
對(duì)chrome的抓包:
通過wireshak可以看到兩個(gè)瀏覽器的到的結(jié)果都是一樣的,Data部分都是e4b8ade59bbd,和我們的預(yù)期一致。不同的是firefox發(fā)送請(qǐng)求用了404個(gè)字節(jié),chrome用了494個(gè)字節(jié),這個(gè)其實(shí)是兩種瀏覽器在發(fā)送請(qǐng)求時(shí),帶的請(qǐng)求頭不一樣,比如用來說明瀏覽器身份的User-Agent請(qǐng)求頭。
既然程序的輸出沒有問題,那就是瀏覽器為什么會(huì)展示成亂碼呢? 我們都知道http程序在吐出內(nèi)容時(shí)還會(huì)攜帶一些響應(yīng)頭,依次來對(duì)內(nèi)容做一些說明,我們上面這段程序攜帶的響應(yīng)頭如下:
可以看到只帶了一個(gè)Content-Length頭用來說明內(nèi)容的字節(jié)長(zhǎng)度,至于如何解釋這六個(gè)字節(jié)瀏覽器是不知道的,所以瀏覽器此時(shí)只能“猜測(cè)”了。首先http協(xié)議本身就是字符型協(xié)議,既然響應(yīng)頭沒有更多的說明,那默認(rèn)就認(rèn)為輸出的內(nèi)容也是字符內(nèi)容了,剩下的問題就是“猜測(cè)”這六個(gè)字節(jié)是那種字符的編碼了。從chrome的顯示可以看到,chrome在瀏覽器窗口中顯示了正確的utf-8編碼,在”response”標(biāo)簽中且使用了錯(cuò)誤的編碼來解釋這六個(gè)字節(jié)。Firefox則正好相反,“響應(yīng)”標(biāo)簽中“猜”對(duì)了編碼,但是瀏覽器窗口中卻使用了錯(cuò)誤的編碼。
需要注意的是這里用“猜測(cè)”這個(gè)詞其實(shí)是不準(zhǔn)確的,實(shí)際上每個(gè)字符編碼都有其特定的規(guī)則,如果對(duì)所有字符編碼規(guī)則都很熟悉,給一段字節(jié)序,是可以推導(dǎo)出它的字符編碼的。
知道了問題所在解決起來就很容易了,在http協(xié)議中有一個(gè)Content-Type頭,用它可以指定內(nèi)容的類型和內(nèi)容的字符編碼?,F(xiàn)在我們?yōu)檩敵黾由享憫?yīng)頭Content-Type:text/plain; charset=utf-8,分別用兩種瀏覽器訪問http://localhost:8080,看到的響應(yīng)頭如下:
此時(shí)firefox的瀏覽器窗口和chrome的“response”標(biāo)簽都顯示了正確的字符。
截止到目前我們得到的結(jié)論應(yīng)該是這樣的,charset指定的編碼需要和輸出內(nèi)容保持一致,這樣在顯示的時(shí)候才不會(huì)出現(xiàn)亂碼。下面我們換一種方式來訪問我們的資源,我們分別使用telnet和curl來訪問http://localhost:8080
通過Telnet來訪問:
因?yàn)槲疫@段web程序并沒有處理任何http的請(qǐng)求頭,它的默認(rèn)動(dòng)作是只要建立好tcp連接后就直接輸出內(nèi)容,所以看到在telnet的時(shí)并沒有發(fā)送任何http協(xié)議需要的請(qǐng)求頭,且依然可以輸出內(nèi)容。
從圖中可以看到,charset=utf-8沒錯(cuò),并且我對(duì)程序沒有做任何的改動(dòng),也就是說程序輸出的編碼和Content-Type指定的編碼是一致的,但我們并沒有看到正確的字符。
通過curl來訪問:
可以看到響應(yīng)頭和內(nèi)容顯示,跟使用telnet訪問時(shí)是一樣的,內(nèi)容都出現(xiàn)了亂碼。
所以我們上面通過瀏覽器訪問資源所得到的,關(guān)于輸出編碼和charset保持一致就不會(huì)出現(xiàn)亂碼的結(jié)論是錯(cuò)誤的嗎?當(dāng)然不是,不過前提是結(jié)論前必須加上“瀏覽器”這個(gè)限定詞。實(shí)際上我們把http的響應(yīng)分成數(shù)據(jù)獲取和數(shù)據(jù)解釋這兩個(gè)步驟就會(huì)很容易理解這問題,首先在數(shù)據(jù)獲取這個(gè)步驟中,瀏覽器、telnet、curl是沒有區(qū)別的,都是和web程序先建立tcp連接,然后獲取web程序返回的數(shù)據(jù)。不同的是在數(shù)據(jù)解釋這個(gè)步驟中,瀏覽器是符合http規(guī)范的,http規(guī)范中說響應(yīng)頭Content-Type中的charset指定的編碼,就是響應(yīng)內(nèi)容的實(shí)際編碼,所以瀏覽器正確的顯示了字符。我們用telnet和curl演示的例子只是負(fù)責(zé)獲取數(shù)據(jù)這一個(gè)步驟,對(duì)于數(shù)據(jù)解釋這個(gè)步驟是有發(fā)起命令的終端來負(fù)責(zé)的,而終端跟http協(xié)議沒有半毛錢關(guān)系,終端只會(huì)只用預(yù)先設(shè)定的編碼規(guī)則來顯示內(nèi)容。
下面是我把終端的編碼設(shè)置為utf-8,然后用curl訪問的結(jié)果:
程序沒有做任何改動(dòng),但是亂碼消失了。
不在響應(yīng)頭中指定編碼規(guī)則就真的不行嗎?
將程序的響應(yīng)頭Content-Type設(shè)置為text/html,不設(shè)置charset,然后分別在兩個(gè)瀏覽器中訪問。
在firefox中訪問:
在chrome中訪問:
可以看到firefox中出現(xiàn)了亂碼,chrome中沒有?,F(xiàn)在我們改動(dòng)一下程序的輸出內(nèi)容,輸出內(nèi)容為:
<html><head><metacharset=”utf-8”></head>中國(guó)</html>
然后再用兩個(gè)瀏覽器分別訪問。
Firfox的訪問:
亂碼消失了。
Chrom的訪問:
顯示正確。
從上面的四張圖可以看到,我們沒有在響應(yīng)頭中指定內(nèi)容的編碼,但仍然沒有出現(xiàn)亂碼問題,原因就在Content-Type:text/html和響應(yīng)內(nèi)容中的<meta charset=”utf-8”>標(biāo)簽,這個(gè)標(biāo)簽對(duì)html內(nèi)容本身做了一個(gè)自描述。想xml這種標(biāo)簽語(yǔ)言也可以像html這樣進(jìn)行自描述,也就是說對(duì)于響應(yīng)是xml的內(nèi)容,即使沒有charset指定編碼,xml也可以通過自描述對(duì)指定正確的編碼。
***需要注意的是,在處理不帶charset的字符內(nèi)容時(shí),瀏覽器不同處理方式也不同,即使相同瀏覽器但版本不一樣,處理方式也不一定一樣。所以我這里介紹的亂碼在你本地不一定會(huì)出現(xiàn),但是為了確保所有瀏覽器不出問題,***在響應(yīng)頭上加上charset并讓其和內(nèi)容的實(shí)際編碼保持一致。如果提供的http資源并不是用在瀏覽器中直接訪問的,而是用來提供接口供各個(gè)系統(tǒng)調(diào)用的,沒有指定charset時(shí)就需要用其它方式來告知對(duì)方內(nèi)容編碼。
請(qǐng)求(Request)過程中出現(xiàn)的亂碼問題
請(qǐng)求過程中出現(xiàn)亂碼時(shí)主要出現(xiàn)在兩個(gè)地方,一個(gè)是請(qǐng)求發(fā)送時(shí)所用的編碼,另一個(gè)是web應(yīng)用接收到請(qǐng)求后解碼時(shí)所有的編碼。請(qǐng)求發(fā)送時(shí)用什么編碼,主要取決于發(fā)送請(qǐng)求所用的客戶端,這里我們以瀏覽器和telnet為客戶端來說明。Web應(yīng)用層我們使用個(gè)tomcat來舉例說明,所以如果你在工作中用的不是tomcat,那么在解碼請(qǐng)求時(shí)會(huì)和這里介紹的解碼行為不一致,但是原理是一樣的,原理明白了也就可以觸類旁通了。
開始之前先解釋下URL的組成:
- {http://localhost:8080[/app/servletpath]}?(name=xxx)
- {}:代表URL
- []:代表URI
- ():代表查詢參數(shù)
對(duì)tomcat使用默認(rèn)設(shè)置,使用如下的代碼來接收請(qǐng)求
- @Override
- public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- System.out.println("name: "+req.getParameter("name"));
- System.out.println("queryString: "+req.getQueryString());
- System.out.println("pathInfo: "+req.getPathInfo());
- System.out.println("requestURL:"+req.getRequestURL());
- }
直接在chrome中輸入http://localhost:8080/app/中國(guó)?name=中國(guó) 得到的結(jié)果如下:
name: ä¸å›½
queryString:name=%E4%B8%AD%E5%9B%BD
pathInfo:/app/ä¸å›½/
requestURL: http://localhost:8080/app/%E4%B8%AD%E5%9B%BD/
從打印的信息可以知道,queryString和請(qǐng)求URL在發(fā)送之前chrome先把中文按照utf-8進(jìn)行了百分號(hào)編碼,關(guān)于百分號(hào)編碼可以看http://deyimsf.iteye.com/blog/2312462:
從里這判斷出請(qǐng)求發(fā)送的時(shí)候編碼是正確的,但是在使用Request.getParameter()和Request.getPathInfo()的時(shí)候出現(xiàn)了解碼錯(cuò)誤。在tomcat文檔中可以看到有URIEncoding一個(gè)參數(shù),文檔對(duì)它的解釋如下:
This specifies the characterencoding used to decode the URI bytes, after %xx decoding the URL. If notspecified, ISO-8859-1 will be used.
大概意思是tomcat會(huì)使用URIEncoding指定的編碼對(duì)URI部分進(jìn)行百分解碼,如果沒有指定則使用ISO-8859-1對(duì)其進(jìn)行解碼。通過這段解釋可以知道,出現(xiàn)亂碼的原因是未用URIEncoding指定正確的編碼。下面我們將URIEncoding設(shè)置為utf-8看會(huì)出現(xiàn)什么結(jié)果,在tomcat的server.xml文件中配置如下:
- <Connectorport="8080" protocol="HTTP/1.1"
- connectionTimeout="20000"
- redirectPort="8443"URIEncoding="utf-8"/>
直接在chrome中輸入http://localhost:8080/app/中國(guó)?name=中國(guó),結(jié)果如下:
name: 中國(guó)
queryString:name=%E4%B8%AD%E5%9B%BD
pathInfo: /app/中國(guó)/
requestURL:http://localhost:8080/app/%E4%B8%AD%E5%9B%BD/
可以看到亂碼消失了,并且入?yún)ame的亂碼也消失了,這說明URIEncoding對(duì)QueryString也是起作用的。
在上面的例子中我們可以看到chrome在發(fā)送請(qǐng)求之前,會(huì)把所有中文進(jìn)行百分號(hào)編碼再發(fā)送出去,并且字符編碼使用的utf-8。實(shí)際上在生產(chǎn)過程中為了保證不出現(xiàn)亂碼,對(duì)請(qǐng)求進(jìn)行百分號(hào)編碼(又叫URL編碼)是必須的,至于為什么要進(jìn)行百分號(hào)編碼,可以看我早前寫的一遍文章http://deyimsf.iteye.com/admin/blogs/1776082:
這篇文章對(duì)為什么要百分號(hào)編碼做了一個(gè)簡(jiǎn)單的解釋。
由于http協(xié)議只規(guī)定請(qǐng)求發(fā)送時(shí)應(yīng)該進(jìn)行編碼,并沒有規(guī)定使用哪種編碼,所以chrome的這種處理方式,并不能代表所有的瀏覽器。僅同一個(gè)請(qǐng)求中的URI部分和Query String部分,有些瀏覽器的編碼方式也有可能是不一樣的。比如我在工作中就遇到過URI 部分使用GBK編碼(沒有進(jìn)行百分號(hào)編碼),而Query String使用的是utf-8進(jìn)行百分號(hào)編碼的瀏覽器。解決這個(gè)問題的辦法就是我們?cè)诎l(fā)送任何請(qǐng)求之前,一定要對(duì)有中文的地方使用某種字符編碼(比如utf-8)對(duì)其進(jìn)行百分號(hào)編碼。
關(guān)于請(qǐng)求體中字符編碼的問題
我們上面說的亂碼問題都出現(xiàn)在URL和Query String中,還有一種容易出現(xiàn)亂碼的問題是在http的請(qǐng)求體中。使用http中的post方法提交表單就可以將入?yún)⒎湃氲秸?qǐng)求體中。
服務(wù)端用于接收post請(qǐng)求的代碼很簡(jiǎn)單,如下:
- @Override
- public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- System.out.println("name:"+ req.getParameter("name"));
- }
非常簡(jiǎn)單,接收到入?yún)⒅笾苯釉诳刂婆_(tái)輸出。
Firefox中進(jìn)行post訪問:
Chrome中進(jìn)行post訪問:
然后在兩個(gè)瀏覽器中分別點(diǎn)擊提交按鈕。
Firefox中提交后,后臺(tái)獲得結(jié)果如下:
name: Öйú
Chrome中提交后,后臺(tái)后的結(jié)果如下:
name: 中國(guó)
兩個(gè)瀏覽器再提交后都出現(xiàn)了亂碼,并且出現(xiàn)了兩種亂碼,因?yàn)榉?wù)端的程序是一樣的,所以從這個(gè)現(xiàn)象我們可以推測(cè)出,兩個(gè)瀏覽器在發(fā)送請(qǐng)求時(shí)使用的編碼肯定是不一樣的,暫時(shí)還看不出是客戶端問題還是服務(wù)端的問題。下面我們使用wireshark來看看兩個(gè)瀏覽器在發(fā)送請(qǐng)求體時(shí),使用的原始編碼是什么。
Firefox發(fā)送請(qǐng)求的wireshark截圖:
Chrome發(fā)送請(qǐng)求的wireshark截圖:
分別看兩張圖的最下面藍(lán)色區(qū)域,可以看到firefox部分是
name=%D6%D0%B9%FA
chrome的部分是
name=%26%2320013%3B%26%2322269%3B
相同的地方是兩個(gè)瀏覽器都對(duì)入?yún)ame的值做了百分號(hào)編碼,不同的是使用的字符編碼不一樣,兩個(gè)瀏覽器發(fā)送請(qǐng)求時(shí),分別使用了自己認(rèn)為是“正確”的字符編碼對(duì)入?yún)⒆隽税俜痔?hào)編碼。有沒有辦法讓不同的瀏覽器在發(fā)送post請(qǐng)求時(shí)使用同一的編碼呢?一種簡(jiǎn)單粗暴的辦法是,我們用js來控制post提交,并且在提交前將所有的入?yún)⒍及凑战y(tǒng)一的字符編碼(如utf-8編碼)做百分號(hào)編碼。
現(xiàn)在來看看另一種辦法,上面我們?cè)趯?duì)請(qǐng)求提交之前為兩個(gè)瀏覽器分別截了兩張圖,可以看到在firefox和chrome獲取表單后的http響應(yīng)頭,這兩張圖的分別只有三個(gè)同樣的響應(yīng)頭Server、Content-Length、Date,現(xiàn)在我們?yōu)檫@個(gè)http響應(yīng)增加一個(gè)Content-Type:text/html; charset=utf-8,然后分別在兩個(gè)瀏覽器中輸入”中國(guó)”并按提交按鈕。
此時(shí)可以看到,兩個(gè)瀏覽器發(fā)送的請(qǐng)求提都變成了
name=%E4%B8%AD%E5%9B%BD
即urf-8形式的百分號(hào)編碼。
兩個(gè)瀏覽器提交后,后臺(tái)獲得的數(shù)據(jù)是
name:ä¸å›½
還是亂碼,只不過現(xiàn)在亂的一樣了。
這里我們后臺(tái)獲取入?yún)⒅档臅r(shí)候,使用了和前面獲取Query String中的入?yún)r(shí)一樣的方法, Request.getParameter(),tomcat中的URIEncoding設(shè)置和前面是一致的,用的是utf-8編碼。瀏覽器發(fā)送請(qǐng)求使用的是同樣編碼規(guī)則,后臺(tái)接收參數(shù)也是使用的同樣的方法,唯一不同的是http請(qǐng)求方法不一樣,一個(gè)get,一個(gè)是post。所以到這里可以得出一個(gè)結(jié)論,URIEncoding對(duì)post方式不起作用。這里需要用到Request.setCharsetEncoding()方法,這個(gè)方法只對(duì)請(qǐng)求體起作用。
服務(wù)端代碼變成如下形式:
- @Override
- public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- req.setCharacterEncoding("utf-8");
- System.out.println("name: "+req.getParameter("name"));
- }
注意Request.setCharsetEncoding()方法一定要放在所有Request.getParameter()等方法之前。
使用Content-Type請(qǐng)求頭指定字符編碼
前面我們一直使用Content-Type作為響應(yīng)頭,來明確響應(yīng)內(nèi)容的字符編碼,其實(shí)這http協(xié)議頭也可以用在請(qǐng)求中,可以用來指定請(qǐng)求體中的字符編碼。
現(xiàn)在我們將服務(wù)端的中的Request.setCharacterEncoding()部分注釋掉,我們使用telnet程序來模擬瀏覽器發(fā)送請(qǐng)求,模擬操作如下:
可以看到為Content-Type頭增加了charset=utf-8設(shè)置。
這時(shí)候在看后端打印出了正確的編碼:
name:中國(guó)
***的出的結(jié)論是,http使用post方式提交表單時(shí),發(fā)送請(qǐng)求所使用的編碼由響應(yīng)頭Content-Type中的charset決定,如果在獲取表單的響應(yīng)中沒有設(shè)置charset,則瀏覽器根據(jù)自身“喜好”來決定。服務(wù)器端在解析請(qǐng)求體內(nèi)容時(shí),解碼編碼用Request.setCharsetEncoding()方法(j2ee)或者請(qǐng)求頭Content-Type來指定。
關(guān)于ISO8859-1的問題
前面我們介紹了三種設(shè)置服務(wù)端解析字符的編碼方式,以此來避免解碼過程中出現(xiàn)的亂碼問題,分別是URIEncoding、setCharsetEncoding()、Content-Type。如果不用這三種方式,那么對(duì)于tomcat來說,它會(huì)默認(rèn)使用ISO8859-1對(duì)字符做解碼。
服務(wù)端程序做如下改造:
- @Override
- public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- System.out.println("name: "+newString(req.getParameter("name").getBytes("iso8859-1"),"utf-8"));
- }
- @Override
- public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- doGet(req, resp);
- }
客戶端我們使用chrome瀏覽器:
其它地方用默認(rèn)值,這其中包括tomcat中不設(shè)置URIEncodng,代碼中沒有Reqeust.setCharsetEncodnig(),請(qǐng)求頭Content-Type中沒有charset。然后用我們前面提到的所有訪問方式,比如多種瀏覽器的get請(qǐng)求、多種瀏覽器的post請(qǐng)求,前提是發(fā)送請(qǐng)求時(shí)一定要對(duì)中文做百分號(hào)編碼。所有這些方式都試過一遍之后你會(huì)發(fā)現(xiàn),不管哪種方式,只要入?yún)ame的值使用的是utf-8編碼(后臺(tái)的doGet方法里用的是utf-8,需要和這里保持一致),后臺(tái)都不會(huì)出現(xiàn)亂碼。是不是感覺很神(詭)奇(異)。下面我們通過走進(jìn)字符編碼的***層,來一起剖析這個(gè)神奇的現(xiàn)象。
如果一個(gè)字符從輸入到輸出出現(xiàn)了亂碼,那么在這個(gè)輸入輸出的中間過程中一定發(fā)生過編碼轉(zhuǎn)換。對(duì)于我們當(dāng)前的測(cè)試用例,發(fā)生了六次編碼轉(zhuǎn)換:
- 瀏覽器對(duì)字符做百分號(hào)編碼
- Tomcat解百分號(hào)編碼
- ISO8859-1編碼轉(zhuǎn)Java內(nèi)碼
- Java內(nèi)碼轉(zhuǎn)ISO8859-1編碼
- 把字節(jié)數(shù)組當(dāng)成utf-8編碼轉(zhuǎn)Java內(nèi)碼
- Java內(nèi)碼轉(zhuǎn)輸出編碼
開始解釋這六次編碼轉(zhuǎn)換之前,先明確一些描述規(guī)則。
- 字符:直接用其字面意思來書寫,比如字符”a”、”中”等
- 字節(jié):用16進(jìn)制加上前綴0x表示,比如ascii字符”a”字節(jié)表示就是0x61
- String.getBytes(“utf-8”):把java內(nèi)碼轉(zhuǎn)成utf-8編碼
- newString(bytes[],”utf-8”): 把字節(jié)數(shù)組當(dāng)成utf-8編碼-轉(zhuǎn)成java內(nèi)碼
瀏覽器對(duì)字符做百分號(hào)編碼
前面我們已經(jīng)知道,對(duì)于”中國(guó)”這兩個(gè)字符,他們的utf-8編碼分別是0xE4B8AD、0xE59BBD,每個(gè)字符占用三個(gè)字節(jié)。經(jīng)過百分號(hào)編碼后變成了%E4%B8%AD、%E5%9B%BD,可以看到百分號(hào)編碼對(duì)原始編碼是無損的,它只是把原始字節(jié)變成了%+原始字節(jié)的16進(jìn)制表示。比如字節(jié)0xE4,轉(zhuǎn)成百分號(hào)編碼為%E4,有一個(gè)字節(jié)變成了三個(gè)字節(jié)。
Tomcat解碼百分號(hào)編碼
解碼百分號(hào)編碼也很簡(jiǎn)單,其實(shí)就是去掉百分號(hào),然后將百分號(hào)后的兩個(gè)字節(jié)合并成一個(gè)字節(jié),如百分號(hào)編碼%E4,解碼后變?yōu)樽止?jié)0xE4。到這一步“中國(guó)”這兩個(gè)字符就變成了0xE4B8AD、0xE59BBD。
ISO8859-1轉(zhuǎn)java內(nèi)碼
ISO8859-1可以簡(jiǎn)單理解為ascii的升級(jí)版本,我們知道ascii只用到了一個(gè)字節(jié)中的后7位,高位始終是0,所以它最多可以表示128個(gè)字符。ISO8859-1可ascii一樣都是單字節(jié)字符集,不同的是它把***位利用起來了,增加了一些西方字符(如±、÷等字符)。
我們這里說的java內(nèi)碼是java程序運(yùn)行時(shí),在內(nèi)存中存儲(chǔ)字符的編碼,用的是unicode標(biāo)準(zhǔn)中定義的utf-16編碼。在java中處理字符就是各種字符編碼轉(zhuǎn)java內(nèi)碼,java內(nèi)碼再轉(zhuǎn)各種字符編碼。舉一個(gè)簡(jiǎn)單的例子,java處理字符類似翻譯官翻譯語(yǔ)言。比如一個(gè)母語(yǔ)是漢語(yǔ),精通日語(yǔ)和英語(yǔ)的翻譯官,他在將日語(yǔ)轉(zhuǎn)成英語(yǔ)或英語(yǔ)轉(zhuǎn)成日語(yǔ)時(shí),一定會(huì)先別他們轉(zhuǎn)成母語(yǔ),然后再轉(zhuǎn)成其它語(yǔ)言??吹竭@里你有可能會(huì)說,厲害的翻譯官不需要轉(zhuǎn)成母語(yǔ),或者翻譯官的母語(yǔ)也不是一種,有可能好多種。但是目前我們的大部分計(jì)算機(jī)語(yǔ)言就只有一種母語(yǔ)。
ISO8859-1和java內(nèi)碼(utf-16)介紹完了就可以說轉(zhuǎn)換的問題了。utf-16是一個(gè)把Unicode碼點(diǎn)值編碼成16位(兩個(gè)字節(jié))整數(shù)的序列,它會(huì)把unicode字符編碼成2字節(jié)或四字節(jié)。前面說了ISO8859-1是8位長(zhǎng)的單字節(jié)字符編碼,所以u(píng)tf-16編碼和ISO8859-1編碼是不兼容的,但是utf-16包含ISO8859-1中的所有字符,所以他們的編碼之間也是有對(duì)應(yīng)關(guān)系的。
在上面第2步(Tomcat解碼百分號(hào)編碼)后,“中國(guó)”這兩個(gè)字符在內(nèi)存中是這樣的0xE4B8ADE59BBD,正好六個(gè)字節(jié)。我們知道這其實(shí)是這兩個(gè)字符的utf-8編碼序列,但是由于我們并沒有告訴tomcat這是什么字符編碼序列,所以tomcat就認(rèn)為這是一個(gè)ISO8859-1編碼序列,并把它告訴了java程序,java程序要做的就是把這個(gè)字節(jié)序列按照ISO8859-1轉(zhuǎn)換成utf-16,轉(zhuǎn)換成功后的對(duì)應(yīng)關(guān)系是這樣的:
ISO8859-1 |
0xE4 |
0xB8 |
0xAD |
0xE5 |
0x9B |
0xBD |
UTF-16 |
0x00E4 |
0x00B8 |
0x00AD |
0x00E5 |
0x009B |
0x00BD |
可以看到原本的兩個(gè)字符,在java中變成了六個(gè)字符;原本的六個(gè)字節(jié),在java中變成了12個(gè)字節(jié)。
Java內(nèi)碼轉(zhuǎn)換成ISO8859-1編碼
這一步驟實(shí)際上是在執(zhí)行我們例子程序中
- System.out.println("name: "+newString(req.getParameter("name").getBytes("iso8859-1"),"utf-8"));
getBytes(“iso8859-1”)這個(gè)方法,也就是把utf-16轉(zhuǎn)換成ISO8859-1。有第三步(ISO8859-1轉(zhuǎn)java內(nèi)碼)中的對(duì)應(yīng)表格可以看到,utf-16轉(zhuǎn)ISO8859-1只需要把每個(gè)字符前面的8位0去掉就可以了,轉(zhuǎn)換成功后倆個(gè)字符就又變成了0xE4B8ADE59BBD。雖然兩次轉(zhuǎn)換過程中,對(duì)字節(jié)的解釋是錯(cuò)誤的,但是并沒有丟失原始字節(jié)信息。
把字節(jié)數(shù)組當(dāng)成utf-8編碼轉(zhuǎn)java內(nèi)碼
這一步執(zhí)行的是上面例子程序中的new String(0xE4B8ADE59BBD,”utf-8”)方法,因?yàn)槲覀兊淖止?jié)數(shù)組本來就是utf-8編碼,所以按照utf-8來轉(zhuǎn)碼肯定是沒問題的,轉(zhuǎn)換成功后的對(duì)應(yīng)關(guān)系是這一樣的:
UTF-8 |
0xE4B8AD |
0xE59BBD |
UTF-16 |
0x4E2D |
0x56FD |
到這里“中國(guó)”這兩個(gè)字符在java內(nèi)部才得到了正確的表示。
Java內(nèi)碼轉(zhuǎn)輸出編碼
這一步執(zhí)行的是上面例子程序中的System.out.println(“中國(guó)”)方法,現(xiàn)在“中國(guó)”這兩個(gè)字符在java內(nèi)部用utf-16得到了正確的表示,剩下的***一步就是對(duì)外輸出,也就是對(duì)外翻譯的過程,我們這里用的java自帶的println方法,這個(gè)方法會(huì)根據(jù)當(dāng)前平臺(tái)的自身編碼進(jìn)行輸出,比如你的平臺(tái)環(huán)境是中文,那輸出的可能就是GBK編碼。如果你不想用平臺(tái)編碼,想自己決定輸出編碼,很簡(jiǎn)單
System.out.write(“中國(guó)”.getByte(“字符編碼”));
這樣就可以了。
【本文來自51CTO專欄作者張開濤的微信公眾號(hào)(開濤的博客),公眾號(hào)id: kaitao-1234567】