Java Web中的编码问题(一)
注:部分内容来源于网络
1.为什么需要编码
计算机的底层只认识010101…,这是计算认识的语言。而人类使用语言有汉语、英语、日语等等。人类的语言计算机是不认识的。所以计算机和人如果想交互的话,就需要进行翻译,预定一种规则,例如:一个汉字“远”在计算机中用几个01表示。
而这个规则就是字符集,一个字符集会提供字符到01之间的映射关系。如下图
2.java中需要编码的场景
首先是在I/O中存在的编码,一般是在字符和字节之间的转换。一般在I/O操作时使用的编解码字符集一致的话就不会出现乱码问题。
其次是在内存中存在的编码。String提供了构造方法将byte[]按指定的字符集解码成字符串,一级getByte()按自定字符集进行编码。
3.UTF-8
UTF-8算是现在用的比较多的字符集,它对汉字采用3个字节表示。并且UTF-8能对单个字符的编值进行校验,例如一个utf-8编码的byte[]中如果其中的一个字符被损坏,不会影响其他字符的码值。所以,utf-8更适合网络传输。
4.在java web中设计的编解码
从使用中文的角度来将,有I/O的地方就会设计编码。
数据经过网络传输都是以字节为单位的,所以所有的数据都是能够被序列化为字节的。在java中数据要被序列化,比继续实现Serializable接口。
用户从浏览器发起一个HTTP请求,需要存在编码的地方是URL、Cookie、Parameter。
服务端接收到HTTP请求后要解析HTTP,其中URL、Cookie和POST表单参数需要解码,服务器可能需要读取数据库中的数据、本地或网络中其他地方的文本文件,这些数据都可能存在编码问题。当Servlet处理完所有的请求的数据后,需要将这些数据再编码,通过Socket发送到用户请求的浏览器里,在经过浏览器解码称为文本。
4.1 URL的编解码
用户提交一个URL,在这个URL中可能存在中文,因此需要编码。下图介绍了URL(这里所说的URL和URL是针对Servlet进行描述的,也就是request.getRequestURL()和request.getRequestURI()返回的URL和URI进行描述的)的几个组成部分。
以tomcat作为Servlet Engine为例,把他们分别对应到下面的这些配置文件中。
Port对应在tomcat的<Connectorport=”8080”/>中配置,而Context Path在<Context path=”/examples”/>中配置,ServletPath在Web应用中的web.xml的<url-pattern>中配置,PathInfo是我们请求的具体的Servlet,QueryString是要传递的参数。注意这里是在浏览器直接输入URL,所以是以GET方法请求的,如果通过POST方法请求,QueryString将通过表单方式提交到服务器端。
上图中的ServletPath和QueryString中部分出现了中文,当我们在浏览器中直接输入这个URL时,在浏览器和服务器端时如何编码和解析这个URL呢?
我们通过谷歌浏览器调试观察我们请求的URL的实际内容。
长远的编码结果为E995BF 和 E8BF9C,可知PathInfo是utf-8编码,QueryString也是utf-8编码。置于为什么会有“%”,查阅URL的编码规范RFC3986可知,浏览器编码URL是将非ASCII字符按照某种编码格式编码成16禁止数字后将每个16进制表示的字节前加上“%”,所以最终的URL就成上面的格式。
有的浏览器对PathInfo和QueryString的编码是不一样的,不同的浏览器对PathInfo的编码也可能不一样。
以tomcat为例看一下,Tomcat接收到这个URL是如何解码的。
解析请求的URL是在org.apache.coyote.http11.InternalInputBuffer的parseRequestLine方法中进行的,这个方法把传过来的URL的byte[]设置到org.apache.coyote.Request的相应属性中。这里的URL仍然是byte格式,转成char是在org.apache.catalina.connector.CoyoteAdapter的convertURI方法中。
对URL的URI部分进行解码的字符集是在connector的<Connector URIEncoding=”UTF-8”>中定义的,如果没有定义,那么将以默认编码ISO-8859-1解析。所以有中文URL是最好把URIEncoding设置成UTF-8编码。
对于QueryString的解析过程:
以GET方式HTTP请求的QueryString与以POST方式的HTTP请求的表单参数都是作为Parameters保存的,都通过request.getParameter获取参数值。对它们的解码是在request.getParameter方法第一次调用时进行的。
request.getParameter方法被调用时将会调用org.apache.catalina.connector.Request的parseParameters方法。这个方法将会对GET和POST方式传递的参数进行解码。但是它们的解码字符集有可能不一样。QueryString的解码字符集是在哪里定义的呢。它本身是通过HTTP的Header传到服务端的,并且也在URL中。
QueryString的解码字符集要么是Header中ContentType定义的Charset,要么是ISO-8859-1,要使用ContentType中定义的编码,就要将connector的<Connector URIEncoding=”UTF-8”useBodyEncodingForURI=”true”/>中的useBodyEncodingForURI设置为true。这个项的名字容易让人产生混淆,它并不是对整个URI都采用BodyEncoding进行解码,而仅仅是对QueryString使用BodyEncoding解码,这一点还要特别注意。
在我们的应用程序中,应该尽量避免在URL中使用非ASCII字符,不然很可能会碰到乱码问题。当然我们在服务端最好设置<Connecter/>中的URIEncoding和useBodyEncodingForURI连个参数。
4.2一种不正常的正确编码
我们在前端通过get请求提交了一个参数包含中文,例如:username=长远,在服务器这边通过
request.getParameter获取时,拿到
????
然后我们通过
username = new String(username.getBytes(“ISO-8859-1”),“UTF-8”);
就能拿到正确的中文。
这其中起始发生两次编解码(以谷歌浏览器为例):
(1) 第一次编码,浏览器将“长远”编码成为byte[];
(2) 第一次解码,Tomcat这边接收到这个byte[],没有由于没有设置QueryString的的字符集,以默认字符集ISO-8859-1进行解码,也就是把这个byte[]当成ISO-8859-1格式进行解码。也就是request.getParameter获取到的结果????;
(3) 第二次编码,username.getBytes(“ISO-8859-1”)发生了第二次编码,此时返回的byte[]和浏览器发过来的是一样的,utf-8编码的;
(4) 第二次解码,new String(username.getBytes(“ISO-8859-1”),“UTF-8”),我们通过(3)拿到了实际编码格式utf-8的byte[],再用utf-8进行解码,当然就得到了正确的结果。
这其中多了一次编解码,如在实际生产环境中,如果服务器没有使用正确的字符集去解析前端传过来的中文,通过这种方式进行转码,无疑增加的服务器的负担。