[轉]netty對http協議解析原理

本文主要介紹netty對http協議解析原理,着重講解keep-alive,gzip,truncked等機制,詳細描述了netty如何實現對http解析的高性能。javascript

1 http協議

1.1 描述

       

標示 ASCII 描述 字符
CR  13 Carriage return (回車)  \n
LF  10 Line feed character(換行) \r
SP  32 Horizontal space(空格)  
COLON  58 COLON(冒號) :

http協議主要使用CRLF進行分割。css

1.2 請求包

            

主要包含三部分:請求行(line),請求頭(header),請求正文(body) html

請求行(Line):主要包含三部分:Method ,URI ,協議/版本。 各部分之間使用空格(SP)分割。整個請求頭使用CRLF分割。(好比:POST /1.0.0/_health_check HTTP/1.1 CRLF)前端

請求頭(Header): 格式爲(name :value),用於客戶端請求的描述信息。header之間以CRLF進行分割。最後一個header會多加一個CRLF。( 好比:Connection: keep-alive CRLF CRLF)java

請求正文(body) :裏面主要是Post提交的數據(可支持多種格式,格式在Content-Type定義,長度是在Content-Length裏面定義)。 nginx

1.3 響應包

      

主要包含三部分:狀態行(line),響應頭(header),響應正文(body)算法

狀態行(line):包含三部分:http版本,服務器返回狀態碼,描述信息。以CRLF進行分割。 ( 好比:HTTP/1.1 200 OK CRLF)chrome

響應頭(header) : 格式爲(name :value),用於服務器返回的描述信息。header之間以CRLF進行分割。最後一個header會多加一個CRLF (好比:Content-Type: text/html CRLF Content-Encoding:gzip CRLF CRLF) json

響應正文(body):裏面主要是返回數據(可支持多種格式,格式在Content-Type定義,長度是在Content-Length裏面定義)。 後端

2 chunked介紹

2.1 背景

     HTTP協議一般使用Content-Length來標識body的長度,在服務器端,須要先申請對應長度的buffer,而後再賦值。若是須要一邊生產數據一邊發送數據,就須要使用"Transfer-Encoding: chunked" 來代替Content-Length,也就是對數據進行分塊傳輸。

2.2 Content-Length描述

       1:http server接收數據時,發現header中有Content-Length屬性,則讀取Content-Length 的值,肯定須要讀取body的長度。

       2:http server發送數據時,根據須要發送byte的長度,在header中增長 Content-Length 項,其中value爲byte的長度,而後將byte數據當作body發送到客戶端。

2.3 chunked描述

       1:http server接收數據時,發現header中有Transfer-Encoding: chunked,則會按照truncked協議分批讀取數據。

       2:http server發送數據時,若是須要分批發送到客戶端,則須要在header中加上 Transfer-Encoding: chunked,而後按照truncked協議分批發送數據。

2.4 truncked協議

    

       1:主要包含三部分:chunk,last-chunk和trailer。若是分屢次發送,則chunk有多份。

       2:chunk主要包含大小和數據,大小表示這個這個trunck包的大小,使用16進制標示。其中trunk之間的分隔符爲CRLF。

       3:經過last-chunk來標識chunk發送完成。 通常讀取到last-chunk(內容爲0)的時候,表明chunk發送完成。

       4:trailer 表示增長header等額外信息,通常狀況下header是空。經過CRLF來標識整個chunked數據發送完成。

 2.5 優勢

      1:假如body的長度是10K,對於Content-Length則須要申請10K連續的buffer,而對於Transfer-Encoding: chunked能夠申請1k的空間,而後循環使用10次。節省了內存空間的開銷。

      2:若是內容的長度不可知,則可以使用trunked方式能有效的解決Content-Length的問題

      3:http服務器壓縮能夠採用分塊壓縮,而不是整個快壓縮。分塊壓縮能夠一邊進行壓縮,通常發送數據,來加快數據的傳輸時間。

 2.6 缺點

     1:truncked 協議解析比較複雜。

     2:在http轉發的場景下(好比nginx) 難以處理,好比如何對分塊數據進行轉發。

3 壓縮  

3.1 背景

     在http請求(特別是移動端),若是請求的資源比較多,則網絡的開銷會比較大,用戶體驗較差。則能夠開啓數據的無損壓縮,節省傳輸的流量,提高數據的加載性能。

3.2 壓縮類型

    1:壓縮須要客戶端,服務器端同時支持。在chrome中,請求默認會加上Accept-Encoding: gzip, deflate,客戶端默認開啓數據壓縮。而tomcat默認關閉壓縮,若是開啓須要增長配置。

    2:在請求時,須要經過header的Accept-Encoding: gzip, deflate 來告訴服務器客戶端支持的壓縮類型。

    3:在返回時,http server會在返回的header中添加Content-Encoding: gzip 來告訴客戶端數據的壓縮方式。

    4:壓縮類型主要包含以下幾種:

          gzip      說明body採用GNU zip編碼

         compress 說明body採用Unix的文件壓縮程序

         deflate  說明body是用zlib的格式壓縮的

         identity  說明沒有對實體進行編碼。

    其中 gzip, compress, 以及deflate編碼都是無損壓縮算法,不會致使信息損失。 gzip效率最高,使用較爲普遍。

3.3 tomcat實現

     tomcat默認是關閉gzip壓縮,開啓須要在server.xml中的Connector標籤中加以下配置:  

              compression=」on」 打開壓縮功能;

           compressionMinSize=」2048″ 啓用壓縮的閾值,只有數據量小於2048 纔會對內容進行壓縮;

           noCompressionUserAgents=」gozilla, traviata」 對於如下的瀏覽器,不啓用壓縮 ;

           compressableMimeType="text/html,text/xml,text/plain,text/css,text/JavaScript,text/json,application/x-javascript,application/javascript,application/json"  壓縮類,只有Content-Type爲設置的類型,纔會進行壓縮。

 

     是否進行壓縮主要是從:數據的大小,瀏覽器的類型和內容的類型來控制。

3.4 優勢

      減小流量,幫公司節省帶寬及流量,幫用戶節省流量

      客戶端(特別是移動端),加載速度變快,提高用戶體驗。

3.5 缺點

     服務器端須要更多的cpu資源進行計算,會下降服務器的總體吞吐量

     服務器端須要多申請更多的內存資源。數據1k的話,不開啓壓縮,只須要申請1k的buffer; 而開啓壓縮的話(假設壓縮後的大小爲250B),則須要多申請250B的buffer,而且涉及到數據的拷貝

    客戶端也須要消耗更多的cpu來進行數據的解壓縮。

4 keepalive

     具體可參考: http://blog.csdn.net/hetaohappy/article/details/51851880

5 粘包,拆包

5.1背景

    TCP是基於stream機制,其實就是一串沒有邊界的數據流。 這裏主要面臨兩個問題:1:如何定義數據的邊界 2:拆包和粘包的問題。HTTP協議是基於TCP,因此也會面臨前面兩個問題。

5.2 數據讀取流程

           

   1:發送端發送數據,數據先經過網卡到服務端tcp的receive buffer中。服務端的上層應用若是須要讀取數據,會申請一段業務buffer,調用JDK的IO接口,IO會將tcpreceive  buffer的數據拷貝到業務的buffer裏面。上層業務再經過設定的反序列化協議將業務buffer轉換成對象進行業務處理。

   2:服務端讀取數據時,先申請一段業務buffer(大小通常是1k),經過調用JDK的channel.read(buffer) IO方法,IO會將tcp buffer的數據拷貝到業務buffer裏面。返回值爲讀取字節的個數:若是返回值大於0,說明讀取到了對應大小的數據;若是是0,表示沒有讀到數據,數據讀取完成(可能業務buffer是滿的,不能往裏面寫數據);若是是-1,表明tcp鏈接被關閉(通常處理是關閉到該鏈接)

  3:在Java裏面能夠設置socket的SO_RCVBUF 參數來設置buffer的大小。默認值保存在:cat /proc/sys/net/core/rmem_default 也可經過cat /proc/sys/net/ipv4/tcp_wmem查看。

5.3 粘包拆包說明

         

  說明:假如服務端連續接收了4個包。 應用申請1k的buffer空間去讀取tcp數據。讀取的流程以下。

    1:業務先申請1k大小的業務buffer,先調用JDK IO接口,會拷貝Receive Buffer的1k數據到業務的buffer裏面。

    2:每一個包定義有邊界。經過邊界定義,讀取到包1和包2分別進行反序列化的處理,轉換爲對象供上層應用處理。(解決粘包的問題)

    3:以下圖:在讀取到包3的時候,因爲把buffer讀完尚未發現邊界。便將包3(剩下的10個)的數據拷貝到buffer的最前端。而後再調用JDK IO接口,tcp receive buffer拷貝數據是從業務buffer的第10個位置進行拷貝賦值。拷貝完後再讀取包3的數據,直到邊界(解決拆包的問題)

                        

      4:而後讀取包4,發現到邊界後,而且數據沒有可讀的,則整個流程結束。

5.4 http解決方案:

      1:請求行的邊界是CRLF,若是讀取到CRLF,則意味着請求行的信息已經讀取完成。

      2:Header的邊界是CRLF,若是連續讀取兩個CRLF,則意味着header的信息讀取完成。

      3:body的長度是有Content-Length 來進行肯定。若是沒有Content-Length ,則是chunked協議(具體參考前面的trunked協議)。

6 netty實現

6.1 http協議實現的抽象

      不少http server(好比tomcat,resin)的實現都是基於servlet,可是netty對http實現並無基於servlet。

     下面將對請求request的抽象進行描述。 response對象的抽象比較相似,將不作描述。

        :

    HttpMethod:主要是對method的封裝,包含method序列化的操做

    HttpVersion: 對version的封裝,netty包含1.0和1.1的版本

    QueryStringDecoder: 主要是對url進行封裝,解析path和url上面的參數。(Tips:在tomcat中若是提交的post請求是application/x-www-form-urlencoded,則getParameter獲取的是包含url後面和body裏面全部的參數,而在netty中,獲取的僅僅是url上面的參數)

    HttpHeaders:包含對header的內容進行封裝及操做

    HttpContent:是對body進行封裝,本質上就是一個ByteBuf。若是ByteBuf的長度是固定的,則請求的body過大,可能包含多個HttpContent,其中最後一個爲LastHttpContent(空的HttpContent),用來講明body的結束。

   HttpRequest:主要包含對Request Line和Header的組合

   FullHttpRequest: 主要包含對HttpRequest和httpContent的組合

 

6.2 request的流程處理

6.2.1 實現:

   只須要在netty的pipeLine中配置HttpRequestDecoder和HttpObjectAggregator。

6.2.2 原理:

     

   1:若是把解析這塊理解是一個黑盒的話,則輸入是ByteBuf,輸出是FullHttpRequest。經過該對象即可獲取到全部與http協議有關的信息。

    2:HttpRequestDecoder先經過RequestLine和Header解析成HttpRequest對象,傳入到HttpObjectAggregator。而後再經過body解析出httpContent對象,傳入到HttpObjectAggregator。當HttpObjectAggregator發現是LastHttpContent,則表明http協議解析完成,封裝FullHttpRequest。

   3:對於body內容的讀取涉及到Content-Length和trunked兩種方式。兩種方式只是在解析協議時處理的不一致,最終輸出是一致的。

6.2.3 面臨的問題:

     1:假設申請的ByteBuf爲1k,若是讀取request Line,把ByteBuf都讀取完了尚未發現邊界(CRLF),如何處理?

     通常的作法爲:先申請1k大小的ByteBuf,若是發現當前ByteBuf大小不夠。 通常會再申請以前大小2倍的ByteBuf(也就是2k),而後把以前1k的數據拷貝到新申請的2k的空間裏面,而後再到JDK的io中讀取數據。若是再不夠用,則再申請2倍的byteBuf。  若是數據量比較大,會面臨着申請新空間->拷貝數據->申請更大的空間->再拷貝數據....   。該種方案性能極其低下,如何提高性能?

    2:若是申請的buffer在堆上面,因爲該buffer存活週期很短,會形成頻繁的GC,影響系統性能。

6.2.4 性能優化:

    1:使用堆外內存,也就是DirectBuffer。來減小GC的次數。

    2:使用buffer pool,避免頻繁的申請及釋放內存。通常pool有兩層,ThreadLocal的pool和全局的pool。 申請buffer空間時,先看ThreadLocal是否有未使用的buffer,若是沒有,再從全局的pool中獲取buffer。通常的內存管理策略是pool裏面的buffer大小所有一致(好比1k),可是 若是須要申請2k的空間,必需要新建2k空間的buffer。若是頻繁申請大於1K空間內存,則性能比較低下。 netty爲了解決該問題,使用了較爲複雜的內存管理策略,具體可參考 http://blog.csdn.net/youaremoon/article/details/47910971 

    3:零拷貝:前面提到拷貝數據的性能問題,採用零拷貝機制可有效解決該問題

    CompositeByteBuf(組合): 好比讀取request Line,申請1k的空間ByteBuf,若是沒有發現邊界(CRLF)。再申請1k的空間ByteBuf到JDK的io中讀取數據。將老的ByteBuf和新申請的ByteBuf組合成CompositeByteBuf,更改CompositeByteBuf的讀寫指針來避免數據的拷貝。

         

     slice(切分):  好比在1k的ByteBuf裏面先讀取requestLine,Header進行解析對象,最後讀取body。因爲body的數據還須要保存在內存裏面供業務使用。通常的作法是新申請一塊空間,將body的數據拷貝到新申請的空間上。這裏經過虛擬一個ByteBuf,而後將讀寫的指針指向真實的ByteBuf的body區域上面,來避免數據的拷貝。

          

 

6.3 response的流程處理

6.3.1實現

   只須要在netty的pipeLine中配置HttpResponseEncoder 

6.3.2原理

         

     1:輸入是FullHttpResponse對象,輸出是ByteBuf。socket再將ByteBuf數據發送到訪問端。

     2:對FullHttpResponse按照http協議進行序列化。判斷header裏面是ContentLength仍是Trunked,而後body按照相應的協議進行序列化。 

     3:具體原理和request請求方式比較相似,此次再也不詳細描述。    

6.4 壓縮實現

6.4.1 實現

       在HttpResponseEncoder以前加上 HttpContentCompressor 。response對象先進過HttpContentCompressor 壓縮後,再通過HttpResponseEncoder進行序列化。

         

       1:壓縮主要是針對body進行壓縮。http1.1不支持對header的壓縮。

        2:壓縮後body的輸出是trunked,而不是Content-length的形式。

6.4.2 Gzip格式

      gzip壓縮後主要包含三部分:

    

      gzip頭:主要存儲的是gzip的壓縮方式

      deflate編碼:內容採用的是deflate壓縮算法

      gzip尾:主要是採用CRC32算法對編碼內容進行校驗。

 

7 安全配置

參數 推薦 返回錯誤碼 描述
requst Line size 2k 414 主要是限制url的長度
header size 4k 414 避免header過長
body size 60M 413 此處通常和業務關聯,通常設置相對較大
keepalive timeout 75   若是鏈接在設定時間內沒有使用,則關閉掉鏈接,避免維護的鏈接過多

GET和POST的區別,筆者以前理解的其中一項是:get的url長度有限制,post的body長度沒有限制。   

其實這種理解是有誤差的:不論是url長度限制或者body長度限制都是有後端http容器配置的。 body的長度限制通常比get的url長度限制稍大。

相關文章
相關標籤/搜索