歡迎點擊「算法與編程之美」↑關注咱們!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出現問題的緣由
若是您對算法或編程感興趣,歡迎掃描下方二維碼並關注公衆號「算法與編程之美」,和您一塊兒探索算法和編程的神祕之處,給您不同的解題分析思路。