自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從瀏覽器到服務(wù)端的中文亂碼深入分析

開發(fā) 開發(fā)工具
前段時(shí)間陸陸續(xù)續(xù)有一些同事跟我詢問中文亂碼問題,每個(gè)人的問題也都大同小異。而我最早之前也一直想寫一篇這樣的文章,無奈都騰不出富裕的時(shí)間,或者說拖延癥比較嚴(yán)重(其實(shí)還是懶),這次就索性對(duì)自己狠一把,對(duì)這個(gè)問題做一個(gè)總結(jié)。

概述

前段時(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的組成:

  1. {http://localhost:8080[/app/servletpath]}?(name=xxx)  
  2. {}:代表URL 
  3. []:代表URI  
  4. ():代表查詢參數(shù) 

對(duì)tomcat使用默認(rèn)設(shè)置,使用如下的代碼來接收請(qǐng)求

  1. @Override  
  2. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3. System.out.println("name: "+req.getParameter("name"));  
  4. System.out.println("queryString: "+req.getQueryString());  
  5. System.out.println("pathInfo: "+req.getPathInfo());  
  6. 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文件中配置如下:

  1. <Connectorport="8080" protocol="HTTP/1.1"  
  2. connectionTimeout="20000"  
  3. 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)單,如下:

  1. @Override  
  2. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3. 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ù)端代碼變成如下形式:

  1. @Override  
  2. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3. req.setCharacterEncoding("utf-8");  
  4. 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ù)端程序做如下改造:

  1. @Override  
  2. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3. System.out.println("name: "+newString(req.getParameter("name").getBytes("iso8859-1"),"utf-8"));  
  4.  
  5. @Override  
  6. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  7. 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)換:

  1. 瀏覽器對(duì)字符做百分號(hào)編碼
  2. Tomcat解百分號(hào)編碼
  3. ISO8859-1編碼轉(zhuǎn)Java內(nèi)碼
  4. Java內(nèi)碼轉(zhuǎn)ISO8859-1編碼
  5. 把字節(jié)數(shù)組當(dāng)成utf-8編碼轉(zhuǎn)Java內(nèi)碼
  6. 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í)行我們例子程序中

  1. 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】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來源: 51cto專欄
相關(guān)推薦

2013-11-20 14:09:37

重繪重排瀏覽器

2010-05-10 14:10:34

Unix服務(wù)器

2009-12-28 15:50:23

AnyMedia接入系

2018-06-07 10:45:41

瀏覽器服務(wù)器響應(yīng)

2021-03-18 10:56:59

SpringMVC參數(shù)解析器

2022-04-12 08:30:45

TomcatWeb 應(yīng)用Servlet

2010-09-07 14:21:22

PPPoE協(xié)議

2011-03-23 11:01:55

LAMP 架構(gòu)

2010-07-06 10:11:25

瀏覽器

2009-12-23 09:06:34

網(wǎng)吧路由器

2012-08-14 17:07:13

2020-12-07 06:23:48

Java內(nèi)存

2014-10-30 15:08:21

快速排序編程算法

2009-12-03 10:09:47

PHP獲取客戶端IP

2009-03-05 09:41:54

谷歌瀏覽器中文版

2009-11-30 14:15:17

Cisco路由器配置實(shí)

2011-04-01 15:09:08

MRTG亂碼

2015-06-12 10:03:05

QQ瀏覽器

2011-07-11 14:12:15

瀏覽器

2010-06-23 13:24:00

CSSCSS選擇器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)