欢迎点击「算法与编程之美」↑关注咱们!git
本文首发于微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列博客。web
本文有些地方的描述对某些人来讲可能比较罗嗦,若是想直接进入正题,可阅读“源码分析”节。但本文是本身一步步分析解决问题思路的记录,虽然有些地方的思考还不是很深刻,主要是因为时间不是很充裕(虽然花了三天时间,但感受仍是不够),我会在后续的博文中,结合本身遇到的实际问题或在论坛中看到的别人提出的问题,一步步的带着问题深刻分析tomcat源码,这种带着问题进行源码分析的方式,比较有针对性,不至于让本身迷失在源码的汪洋之中。若是你们对博客格式或其余方面有比较好的建议,欢迎指出,很是感谢。算法
本次源码分析的目标是:apache
弄清楚org.apache.catalina.conector.RequestFacade::getQueryString()以及getParameter(String)的不一样之处及其各自的具体实现,达到此目标即完成任务。编程
问题的引出是因为前些天在oschina上看到的一篇帖子http://www.oschina.net/question/820641_104356,设计模式
起初的分析思路也是受帖子做者的影响,心想出现这种状况是不是由于hashmap destroy encoding致使的,因此就google了一下hashmap encoding,获得一个比较相关的答案数组
http://stackoverflow.com/questions/8427488/hashmap-destroys-encoding,这篇帖子中出现的状况也比较奇怪。浏览器
程序功能描述以下:
从文件A中读取一组以空格为分隔符的的字符串,而后将这些字符串一行一行的写入到另一个文件B中。
如文件A的格式为:
Aaa bbbbb cdefggg …..
文件B的格式为:
Aaa
Bbbbb
Cdefgggg
….tomcat
程序代码:服务器
final StringBuffer fileData = new StringBuffer(1000); final BufferedReader reader = new BufferedReader( new FileReader("fileIn.txt")); char[] buf = new char[1024]; int numRead = 0; while ((numRead = reader.read(buf)) != -1) { final String readData = String.valueOf(buf, 0, numRead); fileData.append(readData); buf = new char[1024]; } reader.close(); String mergedContent = fileData.toString(); mergedContent = mergedContent.replaceAll("\\<.*?>", " "); mergedContent = mergedContent.replaceAll("\\r\\n|\\r|\\n", " "); final BufferedWriter out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream("fileOut.txt"))); final HashMap<String, String> wordsMap = new HashMap<String, String>(); final String test[] = mergedContent.split(" "); for (final String string : test) { wordsMap.put(string, string); } for (final String string : wordsMap.values()) { out.write(string + "\n"); } out.close();
这种状况下,发现文件B中的内容为乱码,而若是将上述程序中的部分代码改成下面这样,则会获得指望的结果。
... for (final String string : test) { out.write(string + "\n"); //wordsMap.put(string, string); } //for (final String string : wordsMap.values()) //{ // out.write(string + "\n"); //} out.close();
出现这种状况的缘由,我也不是很理解,原文中关于该贴的回答,我以为和问题没有任何关系,大多数人都在讲如何解决这个问题,而没有提到出现上述状况的缘由。
通过该贴和其余一些相关帖的了解,我发现引言中提出的问题貌似和hashmap的encoding没有任何关系,可能存在别的缘由,因而本身写了一个简单的servlet来实践一下。
首先是问题重现,我写了一个简单的servlet以下所示:
//请求的url为:http://localhost:8080/demo/1.do?addr=上海
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //System.out.println(req); System.out.println("Request::getParameter(addr) is: "+ req.getParameter("addr")); String queryString = req.getQueryString(); System.out.println("queryString is: "+queryString); String[] params = queryString.split("[=]"); Map<String, String> map = new HashMap<String, String>(); map.put(params[0], params[1]); System.out.println("Map::get(addr) is: "+map.get(params[0])); return; }
在运行的时候,获得的结果是:
getParameter()获得的值是乱码,而经过getQueryString()解析后存放在map中的值是通过utf-8编码的。
对于getParameter()是乱码,这个缘由比较明显,因为浏览器默认的urlencoding通常是utf-8,而tomcat中默认的URIEncoding是ISO-8859-1不是utf-8(为何默认的编码是iso-8859-1?耐心看完本文后,就会明白),当客户端的请求到达tomcat的时候,tomcat就会用其余的编码方式去decode utf-8编码,那么天然就会出现乱码(具体的tomcat是如何处理queryString的,请继续阅读后面的源码分析节),因此解决方法是在tomcat的配置文件server.xml中加入以下配置(URIEncoding="utf-8"):
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="utf-8" />
经过上述配置文件的修改,咱们获得的测试结果以下:
经上述分析,咱们能够得出,getParameter()的值是根据tomcat中设置的URIEncoding编码进行decode后获得的值,而对于getQueryString() tomcat没有对其进行decode操做,保留了原有的urlencoding编码方式。
至此,咱们基本能够推测,出现引言中的状况的缘由是:
因为客户端对http get请求的url编码方式与tomcat中定义的URIEncoding不一致,致使tomcat服务器利用另一种解码方式来解码客户端的url,这样必然会出现中文乱码现象。而放入Map中的字符串为何没有出现乱码?缘由就在于getQueryString()没有对客户端的url进行decode,于是保留了原有的客户端utf-8编码,因此在后面的使用过程当中,若是利用utf-8对其解码,则不会出现中文乱码现象。
通过上述实践,基本能够肯定问题的缘由,但为了进一步的加以验证,我试着分析了一下tomcat在处理getParameter()和getQueryString()的不一样。
因为HttpServletRequest为一接口,故咱们看不到其getParameter()和getQueryString()具体实现,因此咱们首先须要肯定request的具体实现类是什么,咱们在刚才的servlet中加入以下代码:
System.out.pritnln(req);
经过上述打印结果,咱们能够看到其具体实现类为org.apache.catalina.connector.RequestFacade,因此下一步咱们的工做就是具体的分析这个类是如何处理的,也就是分析两个函数的处理流程,一是RequestFacade::getQueryString(),另一个是RequestFacade::getParameter(String)。
首先要得到tomcat的源码,一般的作法是在eclipse中经过egit插件,将远程的git库clone下来,而后再导入工程。
全部的准备工做就绪后,接下来就是具体的源码分析工做了:
从org.apache.catalina.connector.RequestFacade这个类,咱们能够看到,这是一个使用了façade模式的包装类,因此咱们须要先了解一下façade模式的相关知识。
facade模式的核心是为子系统的一组接口提供一个统一的界面,方便客户端使用子系统,客户端也没必要关心子系统的具体实现。
facade设计模式的适用状况:
1. 原来的类提供的接口比较多,也比较复杂,而咱们只须要使用其部分接口;
2. 原类提供的接口只能部分的知足咱们的须要,且不但愿重写一个新类来代替原类;
...
在本文中,RequestFacade是对Request的一个封装,因为Request自己提供的接口很是之多,而本系统中只须要使用其部分功能,在实际分析过程当中,咱们发现Request的具体工做最后delegate到底层的coyote.Request去作。
如何进行源码的阅读和分析?我通常的思路是,先分析正常的处理逻辑,对于那些日志,错误处理,变量定义等等能够先不用关注,从而达到快速了解总体架构或关键流程。
基于上述思路,咱们获得其处理流程以下:
-RequestFacade::getQueryString() -Request::getQueryString() -org.apache.coyote.Request::queryString()::toString()
经过以上分析能够看出,其处理流程比较简单,经过一步步的delegate,最后真正作工做的是coyote.Request,因此咱们接下来只须要分析该类是如何处理。
相关函数源码以下:
@Override public String getQueryString() { if (request == null) { throw new IllegalStateException( sm.getString("requestFacade.nullRequest")); } return request.getQueryString(); }
/** * Return the query string associated with this request. */ @Override public String getQueryString() { return coyoteRequest.queryString().toString(); }
public MessageBytes queryString() { return queryMB; }
coyote.Request::queryString()作的工做很是简单,仅是返回类型为MessageBytes的queryMB字段,但这个字段是什么时候被赋值的呢?这是一个很是有必要弄清的问题,由于极有可能会在赋值以前进行decode操做。
接下来探讨下queryMB是在什么时候被赋值的?
queryMB是org.apache.coyote.Request的一个私有成员变量,其数据类型为MessageBytes,定义以下:
private MessageBytes queryMB = MessageBytes.newInstance();
咱们如何定位queryMB这个变量是在何时赋值的呢?在eclipse中,选中queryMB,点击鼠标右键,选择open call hierarchy,能够看到queryMB在哪些地方被调用,截图以下所示:
从上图能够看出,有三个地方调用了queryMB,分别是:
public MessageBytes queryString() { return queryMB; }
该函数是得到一个queryMB对象,既然得到了该对象,那么颇有可能在得到对象后对其进行某些操做如赋值操做。
public void recycle() { …. queryMB.recycle(); …. }
顾名思义,queryMB.recycle()是对queryMB的从新回收利用,对该对象进行reset操做,和赋值没有任何联系。
public Request() { parameters.setQuery(queryMB); parameters.setURLDecoder(urlDecoder); }
Request()构造函数中,对其成员变量parameters进行了赋值,和queryMB的赋值没有关系。
根据上述三种状况的分析,咱们得出只有在第一种状况最有可能出现赋值操做,因此接下来将继续分析queryString()被哪些函数所调用,以下图所示:
从截图看出共有七个函数调用了queryString(),从函数名,咱们能够简单的判断出,只有parseRequestLine(boolean)这个函数最有可能对其进行赋值,这个函数是解析http请求request line信息。
/** * Read the request line. This function is meant to be used during the * HTTP request header parsing. Do NOT attempt to read the request body * using it. * * @throws IOException If an exception occurs during the underlying socket * read operations, or if the given buffer is not big enough to accommodate * the whole line. * @return true if data is properly fed; false if no data is available * immediately and thread should be freed */ @Override public boolean parseRequestLine(boolean useAvailableData) throws IOException { …. if (questionPos >= 0) { request.queryString().setBytes(buf, questionPos + 1, end - questionPos - 1); request.requestURI().setBytes(buf, start, questionPos - start); } else { request.requestURI().setBytes(buf, start, end - start); } …. }
从上述代码,咱们能够看到,在解析http request line的时候,的确对queryMB进行了操做,直接从inputbuffer中得到字节信息,并对queryMB进行赋值。
request.queryString().setBytes(buf, questionPos + 1, end - questionPos - 1);
由上层的Request一步步的delegate到底层,最后返回coyote.Request::queryMB()字段,而该字段是由底层直接解析http request line信息,并将获得的字节数组直接赋值给coyote.Request::queryMB。
(首先在connector.RequestFacade中调用getQueryString(),而后转交给connector.Request::getQueryString()处理,最后交由最底层的类coyote.Request直接调用getQueryString()返回该对象中保存的类型为MessageBytes的queryMB字段值,而queryMB是在解析http request line的时候,直接获得原始的bytes信息,而后保存在queryMB中,至此,上层调用的getQueryString()返回的是,未经上层任何处理,直接解析Http request line的字节信息。)
RequestFacade::getParameter()分析
(咱们知道在web开发中,处理的比较多的是http get请求和http post请求,对于get请求咱们可直接由url经过getParameter()方法得到,但对于post请求就会有requsetBody,那么tomcat又是如何处理的?请看后续博文分析)
继续上述getQueryString()的思路,咱们先获得getParameter()的正常处理流程,以下:
-RequestFacade::getParameter(String) -Request::getParameter(String) -Request::parseParameters() -coyote.Request::getParameters() -Parameters::setLimit(int) -Parameters::setEncoding(String) -Parameters::handleQueryParameters() -decodedQuery.duplicate(MessageBytes) -Parameters::processParameters(MessageBytes, String) -Parameters::processParameters(byte[],int,int,String) -coyote.Request::getParameters()::getParameter(String) -Parameters::paramHashValues.get(String)
附录中,有上述每一个函数的具体实现源码,有须要的同窗可在此处查看http://my.oschina.net/gschen/blog/120740。
从上述流程,咱们能够看到,最终的处理函数是Parameter::processParameters(byte[],int,int),接下来将重点分析该方法。
Parameter::processParameters(byte[],int,int,String)该函数有四个参数,第一个参数类型是byte[],是handleQueryParameter()函数中,得到一份queryMB的拷贝,而后传给processParameters(MessageBytes,String),再传给processParameters(byte[],int,int,String)
// -------------------- Processing -------------------- /** Process the query string into parameters */ public void handleQueryParameters() { ... try { decodedQuery.duplicate( queryMB ); } catch (IOException e) { // Can't happen, as decodedQuery can't overflow e.printStackTrace(); } processParameters( decodedQuery, queryStringEncoding ); }
第二个和第三个参数类型都为int,分别是queryString的开始位置和queryString的长度以下:
public void processParameters( MessageBytes data, String encoding ) { ... ByteChunk bc=data.getByteChunk(); processParameters( bc.getBytes(), bc.getOffset(), bc.getLength(), getCharset(encoding)); }
第四个参数为String类型,意思是利用何种方式进行解码,若是未定义,则使用默认的编码方式解码( 关于tomcat何时解析配置文件,得到connector节中的URIEncoding编码信息,并传到本函数的encoding,将在后面的博文中一步步的详细阐述 :tomcat源码分析之解析server.xml )。
大体的处理流程是,一步步的解析queryMB,而后将解析到的每个parameter添加到一个HashMap<String, ArrayList<String>>中,最后在这个hashmap中根据name find到本身须要的value。
Parameters::handleQueryParameters()函数中先是获得queryMB的一份拷贝,这样能够避免对queryMB直接操做,破坏原始的信息,接着交由Parameters::processParameters(DecodedQuery, String)处理,最后交由Parameter::processParameters(byte,int,int)处理,该函数第一个参数是queryMB的一份拷贝,函数的基本功能是对该拷贝进行解析,获得一个个的解码后的parameter,再add到paramHashValues这样的一个HashMap<String, ArrayList<String>>中去。
// -------------------- Parameter parsing -------------------- // we are called from a single thread - we can do it the hard way // if needed ByteChunk tmpName=new ByteChunk(); ByteChunk tmpValue=new ByteChunk(); private final ByteChunk origName=new ByteChunk(); private final ByteChunk origValue=new ByteChunk(); CharChunk tmpNameC=new CharChunk(1024); public static final String DEFAULT_ENCODING = "ISO-8859-1"; private static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_ENCODING);
还记得前面提出的默认编码问题吗?您猜对了,就是在这儿定义了默认的default encoding
public static final String DEFAULT_ENCODING = "ISO-8859-1";
基本思想是:遍历字节数组,依次获得name和value值,而后调用urlDecoder对name和value进行解码,最后调用addParameter(name,value)方法添加到Parameter::HashMap<String, ArrayList<string>>中去。
queryString参数解析算法描述
pos: 开始位置 end: 结束位置 while(pos < end) nameStart: 初始化为pos,参数名称开始位置 nameEnd: 初始化为-1,参数名称结束位置,经过nameStart和nameEnd可得到参数名称 valueStart: 初始化为-1,参数值开始位置 valueEnd: 初始化为-1,参数值结束位置,经过valueStart和valueEnd可得到参数值 parsingName:布尔类型,初始化为true,用来标识是否正在解析名称 parameterComplete: 布尔类型,初始化为false,用来标识一个parameter是否解析完成 do swtich(当前位置pos对应的字节) '=': 是否正在解析参数名称,是则nameEnd = pos, parsingName = false, pos++, valueStart = pos; 不然pos++; '&': 是否正在解析参数名称,若是是,则nameEnd=pos,不然valueEnd = pos, pos++; parameterComplete=ture参数总体解析完成 pos++; default: pos++; while(parameter未解析完成 且 pos < end) if(pos == end) if(nameEnd == -1) nameEnd = pos; if(valueStart > -1 && valueEnd == -1) valueEnd = pos; end while
算法点评
上述算法的精髓在于四个位置indicator和两个boolean变量,在完成一次parameter解析后,经过nameStart,nameEnd得到parameter.name的值,经过valueStart, valueEnd得到parameter.value的值,而parameterComplete用来标识一次parameter解析是否完成,parsingName用来标识是否正在解析名称(为何须要这个标识?由于有些时候,parameter.value可能为空如name=&passwd=123这种状况下)。
算法源码
int pos = start; int end = start + len; while(pos < end) { int nameStart = pos; int nameEnd = -1; int valueStart = -1; int valueEnd = -1; boolean parsingName = true; boolean decodeName = false; boolean decodeValue = false; boolean parameterComplete = false; do { switch(bytes[pos]) { case '=': if (parsingName) { // Name finished. Value starts from next character nameEnd = pos; parsingName = false; valueStart = ++pos; } else { // Equals character in value pos++; } break; case '&': if (parsingName) { // Name finished. No value. nameEnd = pos; } else { // Value finished valueEnd = pos; } parameterComplete = true; pos++; break; case '%': case '+': // Decoding required if (parsingName) { decodeName = true; } else { decodeValue = true; } pos ++; break; default: pos ++; break; } } while (!parameterComplete && pos < end); if (pos == end) { if (nameEnd == -1) { nameEnd = pos; } else if (valueStart > -1 && valueEnd == -1){ valueEnd = pos; } } ... }
上述代码经过一次遍历处理,获得nameStart, nameEnd, valueStart, valueEnd四个indicator,这样即可获得name, value值。在获得parameter.name和parameter.value后,接着就须要对其进行urldecode操做,decode完成以后,调用addParameter(name, value)方法将其添加到hashmap中。
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart); if (valueStart >= 0) { tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart); } else { tmpValue.setBytes(bytes, 0, 0); } try { String name; String value; if (decodeName) { urlDecode(tmpName); } tmpName.setCharset(charset); name = tmpName.toString(); if (valueStart >= 0) { if (decodeValue) { urlDecode(tmpValue); } tmpValue.setCharset(charset); value = tmpValue.toString(); } else { value = ""; } try { addParameter(name, value); } catch (IllegalStateException ise) { // Hitting limit stops processing further params but does ... } } catch (IOException e) { ... } tmpName.recycle(); tmpValue.recycle();
上述代码是对tmpName和tmpValue进行urldecode操做,而后将解码后的信息addParameter。
关于decode的一些说明:
在获得name和value后,调用UDecoder对其解码,若是tomcat的server.xml中未定义URIEncoding,则使用默认的"ISO-8859-1"对其进行解码。
在本次源码分析过程当中,尚有一些未解决的问题,将在之后分析的过程当中,逐步的解决。
问题列表:
1. tomcat是在何时加载server.xml配置文件的,获得URIEncoding值的;
2. digest是如何解析xml文件的;
3. 底层的coyote是如何实现的;
.....
下一篇将分析该贴中http://www.oschina.net/question/853764_103942出现问题的缘由
若是您对算法或编程感兴趣,欢迎扫描下方二维码并关注公众号“算法与编程之美”,和您一块儿探索算法和编程的神秘之处,给您不同的解题分析思路。