本文主要介紹netty對http協議解析原理,着重講解keep-alive,gzip,truncked等機制,詳細描述了netty如何實現對http解析的高性能。
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進行分割。
1.2 請求包
主要包含三部分:請求行(line),請求頭(header),請求正文(body)
請求行(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)
請求正文(body) :里面主要是Post提交的數據(可支持多種格式,格式在Content-Type定義,長度是在Content-Length里面定義)。
1.3 響應包
主要包含三部分:狀態行(line),響應頭(header),響應正文(body)
狀態行(line):包含三部分:http版本,服務器返回狀態碼,描述信息。以CRLF進行分割。 ( 比如:HTTP/1.1 200 OK CRLF)
響應頭(header) : 格式為(name :value),用於服務器返回的描述信息。header之間以CRLF進行分割。最后一個header會多加一個CRLF (比如:Content-Type: text/html CRLF Content-Encoding:gzip CRLF CRLF)
響應正文(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長度限制稍大。