本文主要介紹netty對http協議解析原理,着重講解keep-alive,gzip,truncked等機制,詳細描述了netty如何實現對http解析的高性能。javascript
標示 | ASCII | 描述 | 字符 |
---|---|---|---|
CR | 13 | Carriage return (回車) | \n |
LF | 10 | Line feed character(換行) | \r |
SP | 32 | Horizontal space(空格) | |
COLON | 58 | COLON(冒號) | : |
http協議主要使用CRLF進行分割。css
主要包含三部分:請求行(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
主要包含三部分:狀態行(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裏面定義)。 後端
HTTP協議一般使用Content-Length來標識body的長度,在服務器端,須要先申請對應長度的buffer,而後再賦值。若是須要一邊生產數據一邊發送數據,就須要使用"Transfer-Encoding: chunked" 來代替Content-Length,也就是對數據進行分塊傳輸。
1:http server接收數據時,發現header中有Content-Length屬性,則讀取Content-Length 的值,肯定須要讀取body的長度。
2:http server發送數據時,根據須要發送byte的長度,在header中增長 Content-Length 項,其中value爲byte的長度,而後將byte數據當作body發送到客戶端。
1:http server接收數據時,發現header中有Transfer-Encoding: chunked,則會按照truncked協議分批讀取數據。
2:http server發送數據時,若是須要分批發送到客戶端,則須要在header中加上 Transfer-Encoding: chunked,而後按照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數據發送完成。
1:假如body的長度是10K,對於Content-Length則須要申請10K連續的buffer,而對於Transfer-Encoding: chunked能夠申請1k的空間,而後循環使用10次。節省了內存空間的開銷。
2:若是內容的長度不可知,則可以使用trunked方式能有效的解決Content-Length的問題
3:http服務器壓縮能夠採用分塊壓縮,而不是整個快壓縮。分塊壓縮能夠一邊進行壓縮,通常發送數據,來加快數據的傳輸時間。
1:truncked 協議解析比較複雜。
2:在http轉發的場景下(好比nginx) 難以處理,好比如何對分塊數據進行轉發。
在http請求(特別是移動端),若是請求的資源比較多,則網絡的開銷會比較大,用戶體驗較差。則能夠開啓數據的無損壓縮,節省傳輸的流量,提高數據的加載性能。
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效率最高,使用較爲普遍。
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爲設置的類型,纔會進行壓縮。
是否進行壓縮主要是從:數據的大小,瀏覽器的類型和內容的類型來控制。
具體可參考: http://blog.csdn.net/hetaohappy/article/details/51851880
TCP是基於stream機制,其實就是一串沒有邊界的數據流。 這裏主要面臨兩個問題:1:如何定義數據的邊界 2:拆包和粘包的問題。HTTP協議是基於TCP,因此也會面臨前面兩個問題。
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查看。
說明:假如服務端連續接收了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,發現到邊界後,而且數據沒有可讀的,則整個流程結束。
1:請求行的邊界是CRLF,若是讀取到CRLF,則意味着請求行的信息已經讀取完成。
2:Header的邊界是CRLF,若是連續讀取兩個CRLF,則意味着header的信息讀取完成。
3:body的長度是有Content-Length 來進行肯定。若是沒有Content-Length ,則是chunked協議(具體參考前面的trunked協議)。
不少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的組合
只須要在netty的pipeLine中配置HttpRequestDecoder和HttpObjectAggregator。
1:若是把解析這塊理解是一個黑盒的話,則輸入是ByteBuf,輸出是FullHttpRequest。經過該對象即可獲取到全部與http協議有關的信息。
2:HttpRequestDecoder先經過RequestLine和Header解析成HttpRequest對象,傳入到HttpObjectAggregator。而後再經過body解析出httpContent對象,傳入到HttpObjectAggregator。當HttpObjectAggregator發現是LastHttpContent,則表明http協議解析完成,封裝FullHttpRequest。
3:對於body內容的讀取涉及到Content-Length和trunked兩種方式。兩種方式只是在解析協議時處理的不一致,最終輸出是一致的。
1:假設申請的ByteBuf爲1k,若是讀取request Line,把ByteBuf都讀取完了尚未發現邊界(CRLF),如何處理?
通常的作法爲:先申請1k大小的ByteBuf,若是發現當前ByteBuf大小不夠。 通常會再申請以前大小2倍的ByteBuf(也就是2k),而後把以前1k的數據拷貝到新申請的2k的空間裏面,而後再到JDK的io中讀取數據。若是再不夠用,則再申請2倍的byteBuf。 若是數據量比較大,會面臨着申請新空間->拷貝數據->申請更大的空間->再拷貝數據.... 。該種方案性能極其低下,如何提高性能?
2:若是申請的buffer在堆上面,因爲該buffer存活週期很短,會形成頻繁的GC,影響系統性能。
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區域上面,來避免數據的拷貝。
只須要在netty的pipeLine中配置HttpResponseEncoder
1:輸入是FullHttpResponse對象,輸出是ByteBuf。socket再將ByteBuf數據發送到訪問端。
2:對FullHttpResponse按照http協議進行序列化。判斷header裏面是ContentLength仍是Trunked,而後body按照相應的協議進行序列化。
3:具體原理和request請求方式比較相似,此次再也不詳細描述。
在HttpResponseEncoder以前加上 HttpContentCompressor 。response對象先進過HttpContentCompressor 壓縮後,再通過HttpResponseEncoder進行序列化。
1:壓縮主要是針對body進行壓縮。http1.1不支持對header的壓縮。
2:壓縮後body的輸出是trunked,而不是Content-length的形式。
gzip壓縮後主要包含三部分:
gzip頭:主要存儲的是gzip的壓縮方式
deflate編碼:內容採用的是deflate壓縮算法
gzip尾:主要是採用CRC32算法對編碼內容進行校驗。
參數 | 推薦 | 返回錯誤碼 | 描述 |
---|---|---|---|
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長度限制稍大。