Charset编码基础
Charset全称Character Encoding或字符集编码。Charset是将字符(characters)转换成字节(bytes)或者将字节转换成字符的算法。Java内部采用unicode来表示一个字符。将unicode字符转换成字节的过程,称为“编码”;将字节恢复成unicode字符的过程,称为“解码”。
浏览器发送给WEB应用的request参数,是以字节流的方式来表示的。Request参数必须经过解码才能被Java程序所解读。用来解码request参数的charset被称为“输入字符集编码(Input Charset)”;
WEB应用返回给浏览器的response响应内容必须编码成字节流,才能被浏览器或客户端解读。用来编码response内容的charset被称为“输出字符集编码(Output Charset)”。
一般情况下,input charset和output charset是相同的。因为浏览器发送表单数据时,总是采用当前页面的charset来编码的。例如,有一个表单页面,它的“contentType=text/html; charset=GBK
”,那么用户填完全表单并提交时,浏览器会以GBK
来编码用户所输入的表单数据。如果input charset和output charset不相同,服务器就不能正确解码浏览器根据output charset所发回给WEB应用的表单数据。
然而有一些例外情况下面,输入和输出的charset可能会不同:
-
通过Java Script发送的表单,总是用
UTF-8
编码的。这意味着你必须用UTF-8
作为input charset方能正确解码参数。这样,除非output charset也是UTF-8
,否则两者就是不同的。 -
应用间互相用HTTP访问时,可能采用不同的编码。例如,应用A以
UTF-8
访问应用B,而应用B是以GBK
作为input/output charset的。此时会产生参数解码的错误。 -
直接在浏览器地址栏里输入包含参数的URL,根据不同的浏览器和操作系统的设置,会有不同的结果:
-
例如,中文Windows中,无论ie还是firefox,经试验,默认都以
GBK
来编码参数。IE对直接输入的参数,连URL encoding也没做。 -
而在mac系统中,无论safari还是firefox,经试验,默认都是以
UTF-8
来编码参数。
-
Locale和charset的关系
Locale和charset是相对独立的两个参数,但是又有一定的关系。
Locale决定了要显示的文字的语言,而charset则将这种语言的文字编码成bytes或从bytes解码成文字。因此,charset必须能够涵盖locale所代表的语言文字,如果不能,则可能出现乱码。下表列举了一些locale和charset的组合:
Locale和Charset的关系
Locale | 英文字符集 | 中文字符集 | 全字符集 | |||
---|---|---|---|---|---|---|
ISO-8859-1 | GB2312 | Big5 | GBK | GB18030 | UTF-8 | |
en_US (美国英文) | √ | √ | √ | √ | √ | √ |
zh_CN (简体中文) | √ | √ | √ | √ | ||
zh_TW 、zh_HK (台湾中文、香港中文) | √ | √ | √ | √ |
在所有charset中,有几个“全能”编码:
-
涵盖了unicode中的所有字符。然而用
UTF-8
来编码中文为主的页面时,每个中文会占用3个字节。建议以非中文为主的页面采用UTF-8
编码。 -
中文国际标准,和
UTF-8
一样,涵盖了unicode中的所有字符。用GB18030
来编码中文为主的页面时有一定优势,因为绝大多数常用中文仅占用2个字节,比UTF-8
短1/3。然而GB18030
在非中文的操作系统中,有可能不能识别,其通用性不如UTF-8
好。因此仅建议以中文为主的页面采用GB18030
编码。 -
严格说,
GBK
不是全能编码(例如对很多西欧字符就支持不好),也不是国际标准。但它支持的字符数量接近于GB18030
。
UTF-8
GB18030
GBK
在Servlet API中,以下API是和locale和charset有关的。
locale、charset相关的servlet API
HttpServletRequest | ||
---|---|---|
.getCharacterEncoding() | 读取输入编码 | |
.setCharacterEncoding(charset) | 设置输入编码 |
|
.getLocale() | 取得Accept-Language中浏览器首选的locale | |
.getLocales() | 取得所有Accept-Language中所指定的locales |
HttpServletResponse | ||
---|---|---|
.getCharacterEncoding() | 取得输出编码 | |
.setCharacterEncoding(charset) | 设置输出编码 |
|
.getContentType() | 取得content type |
|
.setContentType(contentType) | 设置content type |
|
.getLocale() | 取得输出locale | |
.setLocale(locale) | 设置输出locale |
|
设置locale和charset是一件看起来容易,做起来不容易的事:
-
输入编码必须在第一个读取request参数的调用之前设置好,否则就无效。
-
在Servlet 2.3之前,设置输出参数的唯一方法,是通过设置带有charset定义的content type。这一点在Servlet 2.4以后得到改进,添加了独立的设置输出编码的方法。
解析GET请求的参数
GET请求是最简单的请求方式。它的参数以URL编码的方式包含在URL中。当你在浏览器地址栏中敲入“http://localhost:8081/user/login.htm?name=%E5%90%8D%E5%AD%97&password=password
”这样一个址址的时候,浏览器就会向localhost:8081
服务器出如下HTTP请求:
GET /user/login.htm?name=%E5%90%8D%E5%AD%97&password=password HTTP/1.1 Host: localhost:8081
GET请求中的参数是以application/x-www-form-urlencoded
方式和特定的charset编码的。假如用来编码URL参数的charset与应用的默认charset不同,那么你必须通过特殊的参数来指定charset。
GET /user/login.htm?_input_charset=UTF-8&name=%E5%90%8D%E5%AD%97&password=password HTTP/1.1
可是,上面的请求在不同的Servlet引擎中,会产生不确定的结果。这是怎么回事呢?
原来,尽管调用request.setCharacterEncoding(charset)
这个方法来设置input charset编码,然而根据Servlet API的规范,这个设定只能对request content生效,而不对URL生效。换句话说,request.setCharacterEncoding(charset)
方法只能用来解析POST请求的参数,而不是GET请求的参数。
那么,应该怎样处理GET请求的参数呢?根据URL规范,URL中非US-ASCII的字符必须进行基于UTF-8
的URL编码。然而实际上,从浏览器到服务器,没有人完全遵守这些规范,于是便造成了一些混乱。目前应用服务器端,我们所遇到的,有下面几种不同的解码方案:
服务器对参数进行解码的逻辑
服务器 | 解码的逻辑 |
---|---|
Tomcat 4 |
|
Tomcat 5及更新版 以及搭载Tomcat 5以上版本的JBoss |
|
Jetty Server |
|
综上所述,所有的应用服务器对于POST请求的参数的处理方法是没有差别的,然而对于GET请求的参数处理方法各有不同。
如果不加任何特别的设置,Tomcat最新版是以ISO-8859-1
来解码GET请求的参数,而Jetty却是以UTF-8
来解码的。因此,无论你以哪一种charset来编码GET请求的参数,都不可能在所有服务器上取得相同的结果 ── 除非修改服务器的配置,但这是一件既麻烦又容易出错的事情。