1. 程式人生 > >Jetty9原始碼剖析 - Connection元件 - HttpParser

Jetty9原始碼剖析 - Connection元件 - HttpParser

轉載自ph0ly:http://www.ph0ly.com

一、概念

HttpParser是專門用於解析HTTP協議的類,它能夠完成HTTP/0.9,HTTP/1.0,HTTP/1.1的解析,支援rfc2616、rfc7230兩種規範下的解析。HttpParser解析完成後會呼叫HttpHandler來將資料放到相關的請求行、請求頭或請求體中(當然HttpParser也支援響應的處理,不過是jetty-client包下面的,服務端還是以HttpChannelOverHttp為準)

二、流程圖

由於HttpParser就是單純的一個工具類,不繼承任何類,因此這裡我們就看下它的解析流程圖

流程圖

HttpParser的解析主要包括上面圖中的幾部分,quickStart主要是快速檢測當前的HTTP報文請求行部分是否有效,如果是髒資料直接快速失敗,parseLine則解析請求行(當然也能處理響應頭,不過通常都是解析請求),parseHeaders解析請求頭,parseContent解析請求體

三、解析狀態機

狀態機

HttpParser總共有25種狀態,狀態遷移基本如上圖所示(可能會有細節的遺漏),先後順序從上到下,最開始HttpParser是START狀態,對於請求報文和響應報文解析,分別走左右兩個分支,HEADER相關的部分狀態請求和響應共享,請求體或響應體也是共享解析演算法

狀態機-1

1. 請求報文解析狀態遷移

請求行:START -> METHOD -> SPACE1 -> URI -> SPACE2 -> REQUEST_VERSION

2. 響應報文解析狀態遷移

響應行:START -> RESPONSE_VERSION -> SPACE1 -> STATUS -> SPACE2 -> REASON

3. Header和Body的解析狀態遷移

請求頭

  • HEADER -> HEADER_IN_NAME -> HEADER_VALUE -> HEADER_IN_VALUE ,這種狀態遷移方式一般是使用者自定義Header
  • HEADER -> HEADER_VALUE -> HEADER_IN_VALUE,這種狀態遷移一般是HttpParser使用Trie樹快速匹配到了Header,但是Value還需要進一步獲取
  • HEADER -> HEADER_IN_VALUE,這種狀態遷移一般是HttpParseer使用Trie樹完全匹配,直接進入HEADER_IN_VALUE切換下一個狀態

請求體

  • CONTENT -> END,這種是普通的帶Content-Length頭的報文,HttpParser一直執行CONTENT狀態,直到最後ContentLength達到了指定的數量,則進入END狀態

另一種是chunked分塊傳輸的資料,不清楚總體大小,只知道每一塊的大小,這種常見於大資料量傳輸

  • CHUNKED_CONTENT -> CHUNK_SIZE -> CHUNK -> CHUNK_END -> END,這種狀態遷移是常規的分塊大小比較小的情況走的狀態路線,先拿到分塊大小,然後讀分塊的資料,並繼續CHUNKED_CONTENT,直到讀到CHUNK結束標記進入END狀態
  • CHUNKED_CONTENT -> CHUNK_SIZE -> CHUNK -> CHUNK_TRAILER -> CHUNK_END -> END,這個是rfc規定的一個在CHUNK資料裡面加類似於Header的解析方式,即CHUNK_TRAILER這個狀態,其他基本一致

四、原始碼剖析

1. 快速解析

首先看下請求行在rfc7230中的定義,如下圖

rfc7230-req-line

當發起請求時,我們使用Request Line的格式

rfc7230-status-line

當響應請求時,我們使用Status Line的格式

相信閱讀這篇部落格讀者都能理解,標準的HTTP/1.x協議格式,SP表示Single Space,單空格,HttpParser可以解析請求報文和響應報文

quickStart

如果設定了RequestHandler就認為是請求解析,如果設定了ResponseHandler就認為是響應解析
如果是解析請求報文,就會快速定位Http方法,如果定位成功,先會記錄方法,然後將狀態切換到下一個狀態SPACE1,表示下一次需要解析空格
如果是解析響應報文,先快速定位版本,然後同樣記錄下來,切到下一狀態SPACE1,要求下一次解析空格
如果前面解析都沒成功,如果當前的第一個字元不是空格,就會認為是合法的方法,會直接切換狀態到METHOD或RESPONSE_VERESION,否則就會認為是錯誤的字元,直接報錯

2. 請求行解析

parseLine

這裡截圖就不截全了,很長,簡單說下思路就是我們上面說的狀態機切換,這個parseLine其實是解析開始行(請求行或響應行),我們可以看到是一個while迴圈,判斷這個state狀態的列舉的索引,其實就是前面我們狀態機的狀態順序,通過大小就能確定是否在這個區間
思路比較簡潔,根據狀態命中分支,執行每個狀態下的解析邏輯。例如METHOD這個狀態,就要把緩衝區首行的METHOD拿出來,雖然這裡用了Trie樹加速查詢,不過邏輯就是看下是否有這個已知的METHOD,後面可以判斷是否合法,然後把狀態切到SPACE1,解析下一個空格,然後會做一些校驗,整體HttpParser思路就是這樣,使用state來控制不同分支的程式碼執行。開始行解析完成後,會將首行引數放到請求處理器或相應處理器

startRequest

我們可以看到REQUEST_VERSION狀態下會呼叫_requestHandler.startRequest,其實就是調到了HttpChannelOverHttp,將資料存到其中
對於REASON狀態,會放到_responseHandler.startResponse,同理仍然是把資料放進去

3. 請求頭解析

parseHeaders-1

parseHeaders-2

請求頭解析和上面一樣,通過狀態輪轉,通常是:HEADER -> HEADER_IN_NAME -> HEADER_VALUE -> HEADER_IN_VALUE(當然不一定是這個,可以看下上面的狀態機講解)
HEADER_IN_NAME會拿頭的名稱,HEADER_IN_VALUE會拿頭的值,這些值會暫存到記憶體,當完成一個完整的K/V解析後,解析下一個HEADER的時候才會觸發上一個HEADER的儲存,也就是default分支會利用parsedHeader,將這個資料放到HttpChannelOverHttp的MetaData中,這裡由於篇幅有限就不展開講了
當完成頭部處理後,如上圖的LINE_FEED分支,說明當前頭部已經解析完成,這個時候會仍然是把上一次解析的頭放到HttpChannelOverHttp中去,並切換狀態,如果前面宣告的Content-Length為0就直接認為當前解析以及完成了,會將狀態切位EOF_CONTENT,否則根據頭部標識來處理,如果頭部聲明瞭Content-Length大小,就設定為CONTENT,如果聲明瞭Transfer-Encoding: chunked,說明要分塊傳輸,就會將狀態切為CHUNKED_CONTENT

4. 請求體解析

4.1 定長解析

contentLength

定長解析很簡單,直接根據頭部宣告的Content-Length來讀資料,如果資料Content-Length比實際給的資料小,那就會造成資料截斷,如果Content-Length比實際給的資料大,那就會HttpParser等待下一次的資料讀取並解析,如果資料達到Content-Length大小,就進入END狀態,表示當前Content以及完成處理,並呼叫handleContentMessage,這個方法會讓HttpInput進入eof狀態,也就是沒資料了

4.2 分塊解析

先來看下分塊傳輸的在rfc7230中的定義

rfc7230-chunked

格式就是在Header中宣告Transfer-Encoding: chunked,然後在body體裡面按照上面的格式傳資料,為了讓大家更好理解分塊傳輸,我在網上找了幾張圖,以便理解

chunked

結合rfc7230定義可以看到,分塊資料的每一塊都需要標識當前這塊的長度,以\r\n換行,然後緊接著是chunk塊資料,接著是下一塊資料,最後以0\r\n結束這個塊傳輸

例如下面這種格式:

1
2
3
4
5
6
7
8
9
10
11
12
GET /hello HTTP/1.1
Host: localhost
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n 
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n 
\r\n

這個例子資料分為3塊,第一塊7個位元組,第二塊9位元組,第三塊7位元組,最後以0表示分塊傳輸結束

下面來看下HttpParser的程式碼怎麼處理的

httpparser-chunked

這裡就擷取一小部分CHUNK處理的程式碼,從上面可以看到當狀態遷移至CHUNK時,會呼叫_handler.content(_contentChunk)把讀到的整塊CHUNK資料放到HttpChannelOverHttp,這最終會被放到HttpInput的inputQ裡面去,這樣業務層就能消費到這些資料

五、總結

HttpParser總體來說還是比較複雜的一個元件,需要對HTTP協議的規範有比較深入的理解,這樣才能寫出一個正確的Http伺服器。對這塊感興趣的讀者可以下來仔細研究下HTTP協議本身,特別是rfc7230、rfc2616這兩個協議,完整講述了實現HTTP協議的需要東西,例如長連線、管道化、分塊傳輸等等。這篇文章就寫到這裡,接下來的文章我會帶領大家繼續探索Jetty裡面HttpInput、HttpOutput、HttpGenerator等元件,另外喜歡這篇文章的,覺得對自己有很大成長的,可以來一波打賞,感謝~

六、參考資料

rfc7230
rfc2616
http-trailers-in-jetty
Transfer-Encoding