说是编码问题,准确来说是中文问题,因为英文用户是没有类似困扰的。
1. 概述
平常我们在遇到编码问题时,都是一通百度,然后一顿复制粘贴之后,发现字符串似乎正常了,然后就认为问题得到了解决,弃而不管。既然大家都知道这种行事方式是不对的,那么本文将通过对收集到的资料和实践来尝试探究这其中的知识点。
2. 为何要编码
- 计算机中存储信息的最小单位为一个字节,即8个bit。所以能表示的字符范围是0~255。
- 但人类需要表示的符号太多,无法用一个字节来完全表示。
为了解决以上矛盾,于是出现了一个新的数据结构char
(16bit长度),而从char
到byte
必须进行编码。
3. 常见编码
- ASCII
单字节编码。共计128个,其中 0 ~ 31 为控制字符,32 ~ 126是打印字符。 - ISO-8859-1
依然是单字节编码,共计能表示256个字符。是目前大部分框架的首选字符集。 - GB2312 / GBK
GBK兼容GB2312,编码算法一致,只是GBK包含更多的汉字字符。
经过GB2312编码的汉字都可以用GBK进行解码,反之则不然。 - UTF-8
最理想的中文编码方式,不论从编码效率还是在安全性上。 - UTF-16
表示字符方便,同时也导致空间占用大,不利于网络传输。
4. 一次请求涉及到的编码
终于到了本文的核心内容了,先让我们通览下在一次请求中涉及到的编解码位置:
- 浏览器端发起一个HTTP请求,涉及到编码的是URL,Cookie, Paramerter。
- 服务器端接收到该请求,其中URI,Cookie和POST表单参数需要解码。
- 服务器端涉及到的读取数据库或者本地/网络上的其他文件,这些数据也牵扯到编码问题。
- 服务端处理完请求,还需要将返回值进行编码,通过Socket发送给客户端浏览器,再经过浏览器解码成为文本。
大致流程可以参见如下的图,相信读者一眼就看出来了,此图非原创。
4.1 URL
首先让我们看看URL的组成部分。
以下我们将以IE作为测试对象,研究编解码对我们程序的影响
首先本次进行测试的相关链接为 : http://localhost:9999/strutsDemo/user_夫礼者.action?name=夫礼者
。
而其在IE浏览器下表现如下(PathInfo为UTF-8编码,而QueryString则是进行的GBK编码):
相应的后台代码如下:
public void 夫礼者() throws Exception{
HttpServletRequest request = ServletActionContext.getRequest();
String parameter = request.getParameter("name");
// 先进行GBK解码, 再进行UTF-8编码
String parameter2 = new String("夫礼者".getBytes("GBK"),CharsetUtil.CHARSET_UTF_8);
// ������
System.out.println(parameter2);
// ������
System.out.println(parameter);
// true
System.out.println(parameter2.equals(parameter));
// false
System.out.println(parameter2.equals("夫礼者"));
}
由以上的测试结果我们可以得出如下结论:
每种浏览器对PathInfo和QueryString的编码格式有自己的选择。
- Chrome, 都是UTF-8
- IE ,前者是UTF-8 而后者是GBK,
- FireFox,待测试。
- 虽然在IE下有相应的配置,但似乎并未生效。(IE与Tomcat与HTTP1.1)
- 请求的URL传递到后端,解析工作是在
org.apache.coyote.http11.InternalInputBuffer.parseRequestLine()
中进行的,这个方法将把前端传递过来的URL的byte[]设置到org.apache.coyote.Request
的相应属性中。注意此时的URL依然是byte格式,转成char的操作是在CoyoteAdapter.convertURI()
中完成的。 CoyoteAdapter.convertURI()
中,Tomcat会获取用户在${CATALINA_HOME}/conf/server.xml
中配置的<Connector URIEncoding="utf-8" />
属性来完成 byte到char的转换。- 而对于
<Connector/>
可配置的另外一个属性useBodyEncodingForURI
则仅仅是对URL中的QueryString部分起效——即对QueryString使用BodyEncoding解码,这一点千万注意。 - 最佳实践:
<Connector URIEncoding="utf-8" useBodyEncodingForURI="true" />
4.2 Http Header
Cookie,redirectPath就是Header中的一员。
我们使用Fiddler进行如下测试:
相应的后端代码如下:
final String contentType = request.getHeader("Content-Type");
// 先进行ISO-8859-1编码, 再进行UTF-8解码; 就正常了
String contentType_successDecode = new String(contentType.getBytes("ISO-8859-1"),CharsetUtil.CHARSET_UTF_8);
// application/json;charset=GBK;lq=夫礼è
System.out.println(contentType);
// application/json;charset=GBK;lq=夫礼者
System.out.println(contentType_successDecode);
通过以上,我们可以得出以下结论:
- Header默认是使用
ISO-8859-1
来进行编码的,服务端(Tomcat)以相应的格式进行解码。所以默认情况下,中文将会乱码。 - 对Header中各项的解码工作是在调用
request.getHeader
时进行的(惰性求值)。 - 以上解码的过程中,将调用
MessageBytes.toString()
方法,而通过跟踪其细节,MessageBytes
内部是通过调用ByteChunk.toString
来完成解码的, 而ByteChunk
内部有个字段public static final Charset DEFAULT_CHARSET = B2CConverter.ISO_8859_1
——正是通过它来进行解码。注意ByteChunk
是提供了setCharset
方法的。 - Cookie的解析过程中伴随着校验(源码位置:
Cookies.processCookieHeader()
方法的第340行,更准确点说是在CookieSupport.isV0Separator()
中),所以如果包含中文,则将直接抛出异常。 - 最佳实践:不要在Header中传递非ASCII字符;如果一定要传递,可以先将这些字符用
org.apache.catalina.util.URLEncoder
进行编码来确保信息不丢失。
4.3 POST表单
- 以GET方式HTTP请求的QueryString和以POST方式HTTP请求的表单参数都是作为Paramters保存的,都通过
request.getParameter
来获取相应的参数值。对他们的解码则是在request.getParameter
第一次被调用时进行的(源码位置:org.apache.catalina.connector.Request.getParameter()
,里面很清晰地展示了惰性求值的逻辑,更具体的参见下方的源码补充)。 - POST表单参数是通过HTTP的BODY传递到服务端的,这一点与QueryString是不同的。
- 当我们在页面上单击提交按钮时,浏览器首先根据ContentType的Charset编码格式(默认情况下,)对在表单中填入的参数进行编码,然后提交到服务器端,在服务器端同样也是用ContentType中的字符集进行解码的。所以通过POST表单提交的参数一般不会出现问题,而且这个字符集是我们可以设置的——
request.setCharacterEncoding
。 - 结合第一条和第三条,你一定要在第一次调用
request.getParameter
方法之前就设置好request.setCharacterEncoding
。 - 最佳实践:除非特殊情形,禁止开发人员调用
request.setCharacterEncoding
和response.setCharacterEncoding
方法,并以javax.servlet.Filter
的形式(例如Spring提供的org.springframework.web.filter.CharacterEncodingFilter
)来完成编码格式的设置工作。
4.4 Http Body
用户请求的资源获取完毕之后,所获取到的内容将通过Response
返回给客户端浏览器。这个过程涉及到先编码,再到浏览器解码的流程逻辑。
Response
编码字符集可以通过response.setCharacterEncoding
来设置,它将覆盖request.setCharacterEncoding
的值,并且通过Header的Content-Type返回客户端。- 浏览器接收到返回的Socket流时将通过Content-Type的charset来解码。
- 如果返回的HTTP Header中Content-Type没有设置charset,那么浏览器将根据HTML中的
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
中的charset来解码。 - 如果以上都不满足,浏览器将使用默认的编码格式来解码。
5. 补充
- Java中一个char是16个bit,相当于两个字节。所以如果两个汉字用char表示,将占据4个字节的空间。
- 一段文本的大小,只看字符本身的长度是没有意义的;一样的字符采用不同的编码最终存储的大小也会不同。这一点在作文本压缩时需要特别留意。
- 以下链接中关于字符集的文章是本人在三年多前收集的资料,虽然年代有些久远,但并不影响其权威性,毕竟这个领域更新换代波及面广,所以迭代速度慢。
- 针对
multipart/form-data
类型的参数,也就是上传的文件编码,同样也使用ContentType定义的字符集编码。值得注意的是,上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及字符编码,而真正编码是在将文件内容添加到parameters中时,如果用这个不能编码,则将会使用默认编码ISO-8859-1来编码。 开发人员必须清楚的servlet规范:
HttpServletRequest.setCharacterEncoding()
方法 仅仅只适用于设置post提交的request body的编码而不是设置get方法提交的queryString的编码。该方法告诉应用服务器应该采用什么编码解析post传过来的内容。很多文章并没有说明这一点。HttpServletRequest.getPathInfo()
返回的结果是由Servlet服务器解码(decode)过的。【本人测试返回值为null】HttpServletRequest.getRequestURI()
返回的字符串没有被Servlet服务器decoded过。- POST提交的数据是作为request body的一部分。
网页的Http头中
ContentType("text/html; charset=GBK")
的作用:- 告诉浏览器网页中数据是什么编码;
- 表单提交时,通常浏览器会根据ContentType指定的charset对表单中的数据编码,然后发送给服务器的。
- 这里需要注意的是:这里所说的ContentType是指http头的ContentType,而不是在网页中meta中的ContentType。
- 通过跟踪源码,我们发现struts2的
org.apache.struts2.dispatcher.Dispatcher.prepare()
中,如果发现当前请求是Ajax,则强制使用UTF-8。 - 通过跟踪源码,我们发现Tomcat在获取对request进行解码的字符格式时,会参考HTTP的
Content-Type
Header。细节参见下方的源码Request.getCharacterEncoding()
。
6. 相关源码
正所谓“talk is cheap, show me the code”
// -------- org.apache.catalina.connector.Request.getParameter
/**
* Return the value of the specified request parameter, if any; otherwise,
* return <code>null</code>. If there is more than one value defined,
* return only the first one.
*
* @param name Name of the desired request parameter
*/
@Override
public String getParameter(String name) {
// 惰性解析
if (!parametersParsed) {
// 如果对于query paramters 的字符编码解析有疑问, 可以在如下方法中断点
parseParameters();
}
return coyoteRequest.getParameters().getParameter(name);
}
// -------- org.apache.catalina.connector.Request.parseParameters
/**
* Parse request parameters.
*/
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
parameters.setLimit(getConnector().getMaxParameterCount());
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
// 获取设置的编码
String enc = getCharacterEncoding();
// 通过配置文件设置的; useBodyEncodingForURI
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
if (enc != null) {
parameters.setEncoding(enc);
if (useBodyEncodingForURI) {
parameters.setQueryStringEncoding(enc);
}
} else {
// 使用默认的"ISO-8859-1"
parameters.setEncoding
(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
if (useBodyEncodingForURI) {
parameters.setQueryStringEncoding
(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
}
}
parameters.handleQueryParameters();
...
}
...
}
// -------- org.apache.catalina.connector.Request.getCharacterEncoding
/**
* Get the character encoding used for this request.
*/
public String getCharacterEncoding() {
if (charEncoding != null)
return charEncoding;
// HTTP头部, 从Content-Type里取
charEncoding = ContentType.getCharsetFromContentType(getContentType());
return charEncoding;
}
7. Links
- 《深入分析Java Web技术内幕》 P61
- Java中文编码小结
- 浏览器URL编码
- 深入浅出URL编码
- Windows 记事本的 ANSI、Unicode、UTF-8 这三种编码模式有什么区别?
- 字符,字节和编码
- Wiki - UTF-8
- UTF-8编码规则(转)