(转)深入理解tomcat处理编码的机制

zz from http://hi.baidu.com/atell/blog/item/79af251d1d4d096df724e45b.html

 

本文从tomcat源码的鲜为人知的UDecoder类入手,试图讲解Tomcat内部处理编码的机制。只涉及且不完全涵盖Tomcat处理请求的编码机制,不涉及响应的编码机制。

1. 关于UDecoder
     查看tomcat5和tomcat6版本的源码,可以看到org.apache.tomcat.util.buf.UDecoder类。
这个类有什么特别的地方呢?
     用简单的一句话概括UDecoder类做的事情:修改所输入的字节流, 将每3个以百分号%开头的字节(如“%HH”,占3个字节),转换成1个十六进制的字节码(如“0xHH”,占1个字节),且将加号"+"变成空格“ ”。
如图举例描述了6个以百分号%开头的字节“%D6%D0”变成了2个十六进制的字节码“[0xD6][0xD0]”。(注:我用中括号“[ ]”来表示其中的内容代表的是1个十六进制形式的字节,下同。)

     我们知道UDecoder类的功能后,进一步想知道的是,tomcat用这个类做了什么事情?
答案是:用于修改http请求中的表单参数的值, 将每1个“%HH”(占3个字节)变成“0xHH”(占1个字节),且将加号"+"(占1个字节)变成空格“ ”(占1个字节)。其中表单参数包括:get方式的url参数和post方式的 application/x-www-form-urlencoded的参数。
举个例子,当我们访问这样的url时,http://abc.com/?keywords=%C6%A1%BE%C6,UDecoder类会将“%C6%A1%BE%C6”转换成“[0xC6][0xA1][0xBE][0xC6]”。
第1点,我们了解UDecoder类就好,继续看第2点。
2. 关于tomcat的Character Encoding(参考 http://wiki.apache.org/tomcat/FAQ/CharacterEncoding)   
我们使用tomcat时知道,要用request.getParameter("keywords")来获取get或post的参数值,而当keywords不是ISO-8859-1编码的字符时,为了获取到正确的keywords值,我们知道有以下几种方式可以解决(假设参数以GBK编码):
第一种:自己编程实现转换:
String ISOkeywords = request.getParameter("keywords");//ISOkeywords是乱码     
String keywords = new String(ISOkeywords.getBytes("ISO-8859-1"), "GBK");//keywords=“啤酒”
第二种:设置${tomcat_home}/config/server.xml中的URIEncode配置项的值为“GBK”
第三种:设置${tomcat_home}/config/server.xml中的useBodyEncodingForURI配 置项的值为true,并实现一个SetCharacterEncodingFilter.java,在tomcat的目录 webapps/servlets-examples/WEB-INF/classes/filters /SetCharacterEncodingFilter.java中存在这个范例。

3. 结合UDecoder和Character Encoding,分析访问http://abc.com/?keywords=%C6%A1%BE%C6时,tomcat处理编码的流程。
   tomcat的处理流程大致可以看作以下四个步骤(具体可参见本文最后的流程图):
  (1)访问http://abc.com/?keywords=%C6%A1%BE%C6
  (2)UDecoder将%C6%A1%BE%C6转换为[0xC6][0xA1][0xBE][0xC6],tomcat持有变量bytes=[0xC6][0xA1][0xBE][0xC6]。
  (3)如果设置了URIEncode配置项或者useBodyEncodingForURI配置项为“GBK”,则tomcat会做以下转换:
     String gbkKeywords = new String(bytes,"GBK");//bytes=[0xC6][0xA1][0xBE][0xC6],gbkKeywords=“啤酒”      程序员使用request.getParameter("keywords")得到的就是上面的gbkKeywords,因此是正确的关键字“啤酒”。如下图。  


(4)如果未设置了URIEncode配置项或者useBodyEncodingForURI配置项 ,则tomcat相当于执行了以下转换:
    String keywords = new String(bytes,"ISO-8859-1");//bytes=[0xC6][0xA1][0xBE][0xC6],keywords是乱码
    程序员使用request.getParameter("keywords")得到的就是上面的keywords,因此是乱码。如下图。    
   

   所以,此时我们需要自己编程实现转换:     
   String isoKeywords = request.getParameter("keywords"); //isoKeywords是乱码
   String keywords = new String(isoKeywords.getBytes("ISO-8859-1"), "GBK");//keywords=“啤酒”

4. UDecoder带来的启发
  (1) 使用UDecoder,可以使得经过URLEncoding和未经过URLEncoding的请求都正确的得到解析。当然,前提是,未经过经过URLEncoding的URL编码和未经过URLEncoding的请求的字节码的编码是同样的一种编码,如都是GBK或UTF-8。
如http://search.china.alibaba.com/search/offer_search.htm?keywords=%C6%A1%BE%C6
和http://search.china.alibaba.com/search/offer_search.htm?keywords=[0xC6][0xA1][0xBE][0xC6],经过UDecoder的处理后,keywords都会变成[0xC6][0xA1][0xBE][0xC6],随后流程和第3点说的流程是一致的。
    想象一下,假如没有UDecoder,那么如果要正确处理keywords=%C6%A1%BE%C6和keywords=[0xC6][0xA1][0xBE][0xC6],tomcat就必须分2种情况去处理:
    当keywords=%C6%A1%BE%C6时,tomcat获得字符串“%C6%A1%BE%C6”,然后使用URLDecoder.decode("%C6%A1%BE%C6")得到“啤酒”
    当keywords=[0xC6][0xA1][0xBE][0xC6]时,tomcat直接获取字节码bytes=[0xC6][0xA1][0xBE][0xC6],然后使用new String(bytes, "GBK");得到“啤酒”
   这样远远没有UDecoder的实现那么美观和简单。
20101021补充,总结一下,在写Servlet时,应该怎么做:
  如果URIEncoding=“ISO-8859-1”,而浏览器的keywords可能是中文或urlencoding的,好像这样也行:
String keywords = request.getParameter("keywords");//"????"或"%C6%A1%BE%C6"
String keywords2 = new String(keywords.getBytes("ISO-8859-1"),"GBK");//“中文”或"%C6%A1%BE%C6"
String keywords3 = URLDecoder.decode(keywords2,"GBK");//“中文”
  如果未知URIEncoding=“GBK”还是“ISO-8859-1”,已知浏览器的keywords是urlencoding的:
String keywords = request.getParameter("keywords");//"中文"或"%C6%A1%BE%C6"
String keywords2 = URLDecoder.decode(keywords2,"GBK");//“中文”
  如果未知URIEncoding=“GBK”还是“ISO-8859-1”,已知浏览器的keywords是中文的:
String keywords = request.getParameter("keywords");//"中文"或"???"
String keywords2 = new String(keywords.getBytes("ISO-8859-1"),"GBK");//则不行!
  如果URIEncoding=“GBK”,而浏览器的keywords可能是中文或urlencoding的:
String keywords = request.getParameter("keywords");//“中文” 这样即可。
另外,其实java.net.URLDecoder和UDecoder是类似的,不过URLDecoder是缺陷的。URLDecoder.decode的参数是String,它通过
str.charAt()来解码%HH->[0xHH],然后new String(bytes, 0, pos, enc).而UDecoder.decode()
的参数是字节数组。区别看出来了吗? URLDecoder.decode("???")时,charAt是什么,是“?”,结果还是
URLDecoder.decode("???")="???". 而UDecoder.decode(字节数组)是可以正确解码的。
 
(2) 为了SEO,有些网站会将参数放到pathInfo中,对于pathInfo的参数解析,我们也可以模仿UDecoder机制。据说前段时间百度爬虫访问阿里巴巴时使用的url就是未URLEncoding的(例如http://search.china.alibaba.com/selloffer/啤酒.html,啤酒实际是GBK的字节码[0xC6][0xA1][0xBE][0xC6]),由于我们对未URLEncoding的pathInfo参数无法正确解析,这样导致百度搜索结果中阿里巴巴的条目有乱码。百度的做法是不规范的,但或许使用UDecoder机制去处理pathInfo,就可以避免这种情况。

5. 最后附上一张图,作为总结。

posted on 2017-05-26 15:15  daition  阅读(325)  评论(0编辑  收藏  举报

导航