参考tomcat-5.5.20
1)核心流程涉及到的几个类
1.1)org.apache.catalina.connector.Request
(这个就是我们在servlet中看到的HttpServletRequest的tomcat实现,有一个非常常见的属性ParameterMap parameterMap)
1.2)org.apache.coyote.Request
(原始的tomcat请求数据结构)
1.3)org.apache.tomcat.util.http.Parameters
(我们通过http传递的参数的数据结构)
注意:org.apache.tomcat.util.http.Parameters有非常重要的几个属性:
- String encoding:
用于post数据的编码
- String queryStringEncoding:
用于get数据的编码
- MessageBytes queryMB:
原始的get请求部分的字节
- CoyoteInputStream inputStream
原始的post请求部分的字节(实际上来自coyoteRequest[org.apache.coyote.Request的实例]的inputBuffer)
- MessageBytes decodedQuery:
在对请求字节进行解码时,会拷贝queryMB即decodedQuery是queryMB 涉及请求参数那部分字节的拷贝
这里所说的解码会在下面的请求过程中详细描述
- Hashtable paramHashStringArray:
把解码后的字节(包括key和value)转为字符保存在hashtable 中,编码的选取规则如下:
get请求对应queryStringEncoding,post对应encoding
paramHashStringArray也是HttpServletRequest.getParameterMap的来源
3者的关系如下:
1)包含 2),2)包含 3)
见下图:
2)请求过程
2.1) 浏览器发起请求比如post部分:x=a, get部分:para=%C6%B1%CE%D6%B6%FB(“票沃尔"的gbk url编码)
2.2) tomcat接到请求会填充org.apache.coyote.Request和org.apache.tomcat.util.http.Parameters
此时
- get部分的数据存入的上文提到的queryMB为
[112, 97, 114, 97, 61, 37, 67, 54, 37, 66, 49, 37, 67, 69, 37, 68, 54, 37, 66, 54, 37, 70, 66]
(对应请求para=%C6%B1%CE%D6%B6%FB,对照ascii码表可以看出%C6%B1%CE%D6%B6%FB即
[37, 67, 54, 37, 66, 49, 37, 67, 69, 37, 68, 54, 37, 66, 54, 37, 70, 66])
- post部分的数据存入上文提到的coyoteRequest的inputBuffer中
2.3) tomcat此时还不会解析参数,什么时候解析依赖web应用程序
如果直接使用servlet或者jsp,通常会用到 request.getParameter("para"), 此时会触发解析过程, 目前web应用程序大量的使用
web框架比 如struts等,那么框架会负责触发解析过程,比如调用request.getParameterMap()得到Map,并将Map中
的key-value以框 架特有的方式传递给应用程序的action之类的东东,比如struts2会将Map中的数据通过valuestack注入
到action中的属性
2.4) 参数解析(详见Parameters.parseParameters)
2.4.1)解析get部分请求的参数
2.4.1.1) 定位%和+
2.4.1.2) 将+变为空格,将%后面连续的两个字符 转为字节(通过x2c方法)
于是上文中的[37, 67, 54, 37, 66, 49, 37, 67, 69, 37, 68, 54, 37, 66, 54, 37, 70, 66]
在解码之后变成 :[-58, -79, -50, -42, -74, -5](这就是"票沃尔"的gbk字节)
2.4.1.3)将byte传为字符paramHashStringArray会有key-value即{para:票沃尔},在字符过程会使用编码,
这个编码来自与服务器的设置,比如<connector ..... URIEncoding="GBK" />,tomcat通常会有2个connector,
一个http,一个ajp, 使用哪个取决于服务器的架构。
2.4.2)解析post部分请求的参数,解析过程与get请求一样,详见Parameters.processParameters方法
不同点在于
a) 字节来源
如上文中提到到,post部分的请求数据取自coyoteRequest的inputBuffer,get来自queryMB
b) 字符编码
post编码与服务器设置的那个URIEncoding无关,具体是什么取决与使用
的框架或者filter之类 , 比如框架或者filter之类常常会在触发解析之前调用Request.setCharacterEncoding方法,
就是设置上文中提到的encoding属性,而post参数正是依赖这个encoding属性
注意:Request.setCharacterEncoding设置的是原始org.apache.coyote.Request的characterEncoding,而Parameters
的 encoding 属性取自org.apache.coyote.Request的charEncoding,所以Request.setCharacterEncoding
间接的设置了Parameters的encoding
2.5)将解析好的参数(包括get部分和post部分)以key(字符)-value(字符)的形式写入paramHashStringArray
2.6)将paramHashStringArray拷贝到Request的parameterMap
2.7)parameterMap----》框架或者应用程序
见下图:
3) 总结
3.1)整个过程就是a)服务器获得参数----》b)框架/servlet/filter设置编码(调用Request.setCharacterEncoding方法)
----》c)触发参数解析过程-----》d)应用程序处理参数
3.2)整个过程需保持客户端/浏览器编码和服务器编码一致,否者会会出现乱码(废话)
3.2.1)对于客户端/浏览器来说,如果是get部分,必须显式的编码即urlencode,此编码需和服务器端的URIEncoding保持一致,
如果没办法保持一致,需特殊处理可以考虑在filter框架中调用
request.getCoyoteRequest().getParameters().setQueryStringEncoding("xxencoding")
参考CoyoteAdapter.service方法中的代码“req.getParameters().setQueryStringEncoding(connector.getURIEncoding());”
3.2.2)如果post那部分请求,此时浏览器如何编码,看见网上很多人这么说:
“html文件里如果有段<meta http-equiv="Content-Type" content="text/html; charset=字符集(GBK,utf-8等)"/>,
那么post就 会用此处指定的编码方式编码”
其实不完全是这样,浏览器编码取决获得此页面时的该页面字节编码,比如生成此页面的response流是utf-8,
就算页面有<meta http-equiv="Content-Type" content="text/html; charset=gbk"/> post还是会采用utf-8的编码,
一个很简单的测试方法就是,将某个html保存为utf-8格式,用浏览器打开此页面发起post,
你会看到http header 信息中post数 据的确是utf-8,而不管你得页面的那个<meta http-equiv ....../>
对付post的方式就是要正确的设置Request.setCharacterEncoding方法
3.3) 请必须确保Request.setEncoding在参数解析(request.getParameter/request.getParameterMap等)过程之前,
否则无效,因为通常情况下参数解析就发生一次
3.4)不要以为utf-8和gbk可以相互转化
网上经常会看到这样的代码new String(request.getParameter("name").getBytes("iso-8859-1"),"utf-8或者gbk之类"),
这个确实可行,但如果
new String(request.getParameter("name").getBytes("utf-8"),"gbk")或者反之就难说了,这个取决jdk的编码转化细节
可以做个小测试:
- utf-8---->gbk
代码:
byte[] bytes="票沃尔".getBytes("utf-8");
System.out.println("byte-utf8:"+Arrays.toString(bytes));
System.out.println("byte-utf8:"+Arrays.toString(new String(bytes,"gbk").getBytes("gbk")));
输出:
[-25, -91, -88, -26, -78, -125, -27, -80, -108]
[-25, -91, -88, -26, -78, -125, -27, -80, 63]
当utf-8被误认为gbk后,再想还原就困难了,此时最后一个字节变成了63(ascii码对应?),导致无法还原,
这是因为对汉字来说,utf-8 是3字节,gbk 2字节,3个汉字本来是9个字节,被gbk传为字符后硬凑了一个字节,变成了5个字符,10个字节
有意思的是如果是偶数个汉字是可以复原的
- gbk---->utf-8
还是那3个汉字,代码雷同
本来是[-58, -79, -50, -42, -74, -5],后来居然变成了[-58, -79, -17, -65, -67, -42, -74, -17, -65, -67]
所以如果你以为客户端gbk/utf-8,服务端utf-8/gbk,然后再做个恢复可以,那么就出问题了