自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

netty5 HTTP協(xié)議棧淺析與實(shí)踐

開(kāi)發(fā) 架構(gòu)
HTTP 請(qǐng)求有很多種 method,最常用的就是 GET 和 POST,每種 method 的請(qǐng)求之間會(huì)有細(xì)微的區(qū)別。下面分別分析一下 GET 和 POST 請(qǐng)求。

 閱讀目錄

1. 寫在前面的話

1.1. 關(guān)于netty example

1.2. 關(guān)于github項(xiàng)目

2. HTTP 協(xié)議知多少

2.1. GET請(qǐng)求

2.2. POST請(qǐng)求

2.3. HTTP POST Content-Type

3. netty HTTP 編解碼

3.1. netty 自帶 HTTP 編解碼器

3.2. HTTP GET 解析實(shí)踐

3.3. HTTP POST 解析實(shí)踐

4. 自定義 HTTP POST 的 message body 解碼器

4.1. HttpJsonDecoder

4.2. HttpProtobufDecoder

5. 聊聊開(kāi)發(fā)中遇到的問(wèn)題【推薦】

5.1. 關(guān)于內(nèi)存泄漏

5.1.1. netty 應(yīng)用計(jì)數(shù)對(duì)象

5.1.2. 如何規(guī)避內(nèi)存泄漏

5.2. 關(guān)于 HTTP 長(zhǎng)連接

5.2.1. TCP KeepAlive 和 HTTP KeepAlive

5.2.2. 長(zhǎng)連接方式中如何判斷數(shù)據(jù)發(fā)送完成

1. 說(shuō)在前面的話

前段時(shí)間,工作上需要做一個(gè)針對(duì)視頻質(zhì)量的統(tǒng)計(jì)分析系統(tǒng),各端(PC端、移動(dòng)端和 WEB端)將視頻質(zhì)量數(shù)據(jù)放在一個(gè) HTTP 請(qǐng)求中上報(bào)到服務(wù)器,服務(wù)器對(duì)數(shù)據(jù)進(jìn)行解析、分揀后從不同的維度做實(shí)時(shí)和離線分析。(ps:這種活兒本該由統(tǒng)計(jì)部門去做的,但由于各種原因落在了我頭上,具體原因略過(guò)不講……)

先用個(gè)“概念圖”來(lái)描繪下整個(gè)系統(tǒng)的架構(gòu):

嗯,這個(gè)是真正的“概念圖”,因?yàn)槲乙呀?jīng)把大部分細(xì)節(jié)都屏蔽了,別笑,因?yàn)楸疚牡闹攸c(diǎn)只是整個(gè)架構(gòu)中的一小部分,就是上圖中紅框內(nèi)的 http server。

也許你會(huì)問(wèn),這不就是個(gè) HTTP 服務(wù)器嗎,而且是只處理一個(gè)請(qǐng)求的 HTTP 服務(wù)器,搞個(gè)java web 項(xiàng)目在 Tomcat 中一啟動(dòng)不就完事兒了,有啥好講的呀?。莫慌,且聽(tīng)老夫慢慢道來(lái)為啥要用 netty HTTP 協(xié)議棧來(lái)實(shí)現(xiàn)這個(gè)接收轉(zhuǎn)發(fā)服務(wù)。

  • 首先,接入服務(wù)需要支持10W+ tps,而 netty 的多線程模型和異步非阻塞的特性讓人很自然就會(huì)將它和高并發(fā)聯(lián)系起來(lái)。
  • 其次,接入服務(wù)雖然使用 HTTP 協(xié)議,但顯然這并不是個(gè) WEB 應(yīng)用,無(wú)需運(yùn)行在相對(duì)較重的 Tomcat 這種 WEB 容器上。
  • 接著,在提供同等服務(wù)的情況下對(duì)比 netty HTTP 協(xié)議棧和 Tomcat HTTP 服務(wù),發(fā)現(xiàn)使用 netty 時(shí)在機(jī)器資源占用(如CPU使用率、內(nèi)存占用及上下文切換等)方面要優(yōu)于 Tomcat。
  • 最后,netty 一直在說(shuō)對(duì) HTTP 協(xié)議提供了非常好的支持,因此想乘機(jī)檢驗(yàn)一下是否屬實(shí)。

基于以上幾點(diǎn)原因,老夫就決定使用 netty HTTP 協(xié)議棧開(kāi)干啦~

本文并非純理論或純技術(shù)類文章,而是結(jié)合理論進(jìn)而實(shí)踐(雖然沒(méi)有特別深入的實(shí)踐),淺析 netty 的 HTTP 協(xié)議棧,并著重聊聊實(shí)踐中遇到的問(wèn)題及解決方案。越往后越精彩哦!

1.1. 關(guān)于netty example

netty 官方提供了關(guān)于 HTTP 的例子,大伙兒可以在 netty 項(xiàng)目中查看。

https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http

https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http2

1.2. 關(guān)于github項(xiàng)目

本人在網(wǎng)上使用 “netty + HTTP” 的關(guān)鍵字搜索了下,發(fā)現(xiàn)大部分都是原搬照抄 netty 項(xiàng)目中的 example,很少有“原創(chuàng)性”的實(shí)踐,也幾乎沒(méi)有看到實(shí)現(xiàn)一個(gè)相對(duì)完整的 HTTP 服務(wù)器的項(xiàng)目(比如如何解析GET/POST請(qǐng)求、自定義 HTTP decoder、對(duì) HTTP 長(zhǎng)短連接的思考等等……),因此就自己整理了一個(gè)相對(duì)完整一點(diǎn)的項(xiàng)目,項(xiàng)目地址https://github.com/cyfonly/netty-http,該項(xiàng)目實(shí)現(xiàn)了基于 netty5 的 HTTP 服務(wù)端,暫時(shí)實(shí)現(xiàn)以下功能:

  • HTTP GET 請(qǐng)求解析與響應(yīng)
  • HTTP POST 請(qǐng)求解析與響應(yīng),提供 application/json、application/x-www-form-urlencoded、multipart/form-data 三種常見(jiàn) Content-Type 的 message body 解析示例
  • HTTP decoder實(shí)現(xiàn),提供 POST 請(qǐng)求 message body 解碼器的 HttpJsonDecoder 及 HttpProtobufDecoder 實(shí)現(xiàn)示例
  • 作為服務(wù)端接收瀏覽器文件上傳及保存

將來(lái)可能會(huì)繼續(xù)實(shí)現(xiàn)的功能有:

  • 命名空間
  • uri路由
  • chunked 傳輸編碼

如果你也打算使用 netty 來(lái)實(shí)現(xiàn) HTTP 服務(wù)器,相信這個(gè)項(xiàng)目和本文對(duì)你是有較大幫助的!

好了,閑話不多說(shuō),下面正式進(jìn)入正題。

2. HTTP 協(xié)議知多少

要通過(guò) netty 實(shí)現(xiàn) HTTP 服務(wù)端(或者客戶端),首先你得了解 HTTP 協(xié)議【1】。

HTTP 協(xié)議是請(qǐng)求/響應(yīng)式的協(xié)議,客戶端需要發(fā)送一個(gè)請(qǐng)求,服務(wù)器才會(huì)返回響應(yīng)內(nèi)容。例如在瀏覽器上輸入一個(gè)網(wǎng)址按下 Enter,或者提交一個(gè) Form 表單,瀏覽器就會(huì)發(fā)送一個(gè)請(qǐng)求到服務(wù)器,而打開(kāi)的網(wǎng)頁(yè)的內(nèi)容,就是服務(wù)器返回的響應(yīng)。

下面講下 HTTP 請(qǐng)求和響應(yīng)包含的內(nèi)容。

HTTP 請(qǐng)求有很多種 method,最常用的就是 GET 和 POST,每種 method 的請(qǐng)求之間會(huì)有細(xì)微的區(qū)別。下面分別分析一下 GET 和 POST 請(qǐng)求。

2.1. GET請(qǐng)求

下面是瀏覽器對(duì) http://localhost:8081/test?name=XXG&age=23 的 GET 請(qǐng)求時(shí)發(fā)送給服務(wù)器的數(shù)據(jù):

可以看出請(qǐng)求包含 request line 和 header 兩部分。其中 request line 中包含 method(例如 GET、POST)、request uri 和 protocol version 三部分,三個(gè)部分之間以空格分開(kāi)。request line 和每個(gè) header 各占一行,以換行符 CRLF(即 \r\n)分割。

2.2. POST請(qǐng)求

下面是瀏覽器對(duì) http://localhost:8081/test 的 POST 請(qǐng)求時(shí)發(fā)送給服務(wù)器的數(shù)據(jù),同樣帶上參數(shù) name=XXG&age=23:

可以看出,上面的請(qǐng)求包含三個(gè)部分:request line、header、message,比之前的 GET 請(qǐng)求多了一個(gè) message body,其中 header 和 message body 之間用一個(gè)空行分割。POST 請(qǐng)求的參數(shù)不在 URL 中,而是在 message body 中,header 中多了一項(xiàng) Content-Length 用于表示 message body 的字節(jié)數(shù),這樣服務(wù)器才能知道請(qǐng)求是否發(fā)送結(jié)束。這也就是 GET 請(qǐng)求和 POST 請(qǐng)求的主要區(qū)別。

HTTP 響應(yīng)和 HTTP 請(qǐng)求非常相似,HTTP 響應(yīng)包含三個(gè)部分:status line、header、massage body。其中 status line 包含 protocol version、狀態(tài)碼(status code)、reason phrase 三部分。狀態(tài)碼用于描述 HTTP 響應(yīng)的狀態(tài),例如 200 表示成功,404 表示資源未找到,500 表示服務(wù)器出錯(cuò)。

在上面的 HTTP 響應(yīng)中,Header 中的 Content-Length 同樣用于表示 message body 的字節(jié)數(shù)。Content-Type 表示 message body 的類型,通常瀏覽網(wǎng)頁(yè)其類型是HTML,當(dāng)然還會(huì)有其他類型,比如圖片、視頻等。

2.3. HTTP POST Content-Type

HTTP/1.1 協(xié)議規(guī)定的 HTTP 請(qǐng)求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。其中 POST 一般用來(lái)向服務(wù)端提交數(shù)據(jù),本文討論主要的幾種 POST 提交數(shù)據(jù)方式。

我們知道,HTTP 協(xié)議是以 ASCII 碼傳輸,建立在 TCP/IP 協(xié)議之上的應(yīng)用層規(guī)范。規(guī)范把 HTTP 請(qǐng)求分為三個(gè)部分:狀態(tài)行、請(qǐng)求頭、消息主體。類似于下面這樣:

  1. <method> <request-URL> <version> 
  2. <headers> 
  3. <entity-body> 

協(xié)議規(guī)定 POST 提交的數(shù)據(jù)必須放在消息主體(entity-body)中,但協(xié)議并沒(méi)有規(guī)定數(shù)據(jù)必須使用什么編碼方式。實(shí)際上,開(kāi)發(fā)者完全可以自己決定消息主體的格式,只要最后發(fā)送的 HTTP 請(qǐng)求滿足上面的格式就可以。

但是,數(shù)據(jù)發(fā)送出去,還要服務(wù)端解析成功才有意義。一般服務(wù)端語(yǔ)言如 php、python 等,以及它們的 framework,都內(nèi)置了自動(dòng)解析常見(jiàn)數(shù)據(jù)格式的功能。服務(wù)端通常是根據(jù)請(qǐng)求頭(headers)中的 Content-Type 字段來(lái)獲知請(qǐng)求中的消息主體是用何種方式編碼,再對(duì)主體進(jìn)行解析。所以說(shuō)到 POST 提交數(shù)據(jù)方案,包含了 Content-Type 和消息主體編碼方式 Charset 兩部分。下面就正式開(kāi)始介紹它們。

2.3.1. application/x-www-form-urlencoded

這應(yīng)該是最常見(jiàn)的 POST 提交數(shù)據(jù)的方式了。瀏覽器的原生 Form 表單,如果不設(shè)置 enctype 屬性,那么最終就會(huì)以 application/x-www-form-urlencoded 方式提交數(shù)據(jù)。請(qǐng)求類似于下面這樣(無(wú)關(guān)的請(qǐng)求頭在本文中都省略掉了):

  1. POST http://www.example.com HTTP/1.1 
  2. Content-Type: application/x-www-form-urlencoded;charset=utf-8 
  3. title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3 

首先,Content-Type 被指定為 application/x-www-form-urlencoded;其次,提交的數(shù)據(jù)按照 key1=val1&key2=val2 的方式進(jìn)行編碼,key 和 val 都進(jìn)行了 URL 轉(zhuǎn)碼。大部分服務(wù)端語(yǔ)言都對(duì)這種方式有很好的支持。

很多時(shí)候,我們用 Ajax 提交數(shù)據(jù)時(shí),也是使用這種方式。例如 JQuery 的 Ajax,Content-Type 默認(rèn)值都是 application/x-www-form-urlencoded;charset=utf-8 。

2.3.2. multipart/form-data

這又是一個(gè)常見(jiàn)的 POST 數(shù)據(jù)提交的方式。我們使用表單上傳文件時(shí),必須讓 Form 的 enctyped 等于這個(gè)值。直接來(lái)看一個(gè)請(qǐng)求示例:

  1. POST http://www.example.com HTTP/1.1 
  2. Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA 
  3.  
  4. ------WebKitFormBoundaryrGKCBY7qhFd3TrwA 
  5. Content-Disposition: form-data; name="text" 
  6.  
  7. title 
  8. ------WebKitFormBoundaryrGKCBY7qhFd3TrwA 
  9. Content-Disposition: form-data; name="file"; filename="chrome.png" 
  10. Content-Type: image/png 
  11.  
  12. PNG ... content of chrome.png ... 
  13. ------WebKitFormBoundaryrGKCBY7qhFd3TrwA-- 

這個(gè)例子稍微復(fù)雜點(diǎn)。首先生成了一個(gè) boundary 用于分割不同的字段,為了避免與正文內(nèi)容重復(fù),boundary 很長(zhǎng)很復(fù)雜。然后 Content-Type 里指明了數(shù)據(jù)是以 mutipart/form-data 來(lái)編碼,本次請(qǐng)求的 boundary 是什么內(nèi)容。消息主體里按照字段個(gè)數(shù)又分為多個(gè)結(jié)構(gòu)類似的部分,每部分都是以 –boundary 開(kāi)始,緊接著內(nèi)容描述信息,然后是回車,最后是字段具體內(nèi)容(文本或二進(jìn)制)。如果傳輸?shù)氖俏募?,還要包含文件名和文件類型信息。消息主體最后以 –boundary– 標(biāo)示結(jié)束。

這種方式一般用來(lái)上傳文件,各大服務(wù)端語(yǔ)言對(duì)它也有著良好的支持。

上面提到的這兩種 POST 數(shù)據(jù)的方式,都是瀏覽器原生支持的,而且現(xiàn)階段原生 Form 表單也只支持這兩種方式。但是隨著越來(lái)越多的 Web 站點(diǎn),尤其是 WebApp,全部使用 Ajax 進(jìn)行數(shù)據(jù)交互之后,我們完全可以定義新的數(shù)據(jù)提交方式,給開(kāi)發(fā)帶來(lái)更多便利。

2.3.3. application/json

application/json 這個(gè) Content-Type 作為響應(yīng)頭大家肯定不陌生。實(shí)際上,現(xiàn)在越來(lái)越多的人把它作為請(qǐng)求頭,用來(lái)告訴服務(wù)端消息主體是序列化后的 JSON 字符串。由于 JSON 規(guī)范的流行,除了低版本 IE 之外的各大瀏覽器都原生支持 JSON.stringify,服務(wù)端語(yǔ)言也都有處理 JSON 的函數(shù),使用 JSON 不會(huì)遇上什么麻煩。

JSON 格式支持比鍵值對(duì)復(fù)雜得多的結(jié)構(gòu)化數(shù)據(jù),這一點(diǎn)也很有用,當(dāng)需要提交的數(shù)據(jù)層次非常深,就可以考慮把數(shù)據(jù) JSON 序列化之后來(lái)提交的。

  1. var data = {'title':'test''sub' : [1,2,3]}; 
  2.  
  3. $http.post(url, data).success(function(result) { 
  4.  
  5. ... 
  6.  
  7. }); 

最終發(fā)送的請(qǐng)求是:

  1. POST http://www.example.com HTTP/1.1 
  2.  
  3. Content-Type: application/json;charset=utf-8 
  4.  
  5. {"title":"test","sub":[1,2,3]} 

這種方案,可以方便的提交復(fù)雜的結(jié)構(gòu)化數(shù)據(jù),特別適合 RESTful 的接口。各大抓包工具如 Chrome 自帶的開(kāi)發(fā)者工具、Fiddler,都會(huì)以樹形結(jié)構(gòu)展示 JSON 數(shù)據(jù),非常友好。

其他幾種 Content-Type 就不一一詳細(xì)介紹了,感興趣的童鞋請(qǐng)自行了解。下面進(jìn)入 netty 支持 HTTP 協(xié)議的源碼分析階段。

3. netty HTTP 編解碼

要通過(guò) netty 處理 HTTP 請(qǐng)求,需要先進(jìn)行編解碼。

3.1. netty 自帶 HTTP 編解碼器

netty5 提供了對(duì) HTTP 協(xié)議的幾種編解碼器:

3.1.1. HttpRequestDecoder

  1. Decodes ByteBuf into HttpRequest and HttpContent. 

即把 ByteBuf 解碼到 HttpRequest 和 HttpContent。

3.1.2. HttpResponseEncoder

  1. Encodes an HttpResponse or an HttpContent into a ByteBuf. 

即把 HttpResponse 或 HttpContent 編碼到 ByteBuf。

3.1.3. HttpServerCodec

  1. A combination of HttpRequestDecoder and HttpResponseEncoder which enables easier server side HTTP implementation. 

即 HttpRequestDecoder 和 HttpResponseEncoder 的結(jié)合。

因此,基于 netty 實(shí)現(xiàn) HTTP 服務(wù)端時(shí),需要在 ChannelPipeline 中加上以上編解碼器:

  1. ch.pipeline().addLast("codec",new HttpServerCodec()) 

或者

  1. ch.pipeline().addLast("decoder",new HttpRequestDecoder()) 
  2.  
  3. .addLast("encoder",new HttpResponseEncoder()) 

然而,以上編解碼器只能夠支持部分 HTTP 請(qǐng)求解析,比如 HTTP GET請(qǐng)求所傳遞的參數(shù)是包含在 uri 中的,因此通過(guò) HttpRequest 既能解析出請(qǐng)求參數(shù)。但是,對(duì)于 HTTP POST 請(qǐng)求,參數(shù)信息是放在 message body 中的(對(duì)應(yīng)于 netty 來(lái)說(shuō)就是 HttpMessage),所以以上編解碼器并不能完全解析 HTTP POST請(qǐng)求。

這種情況該怎么辦呢?別慌,netty 提供了一個(gè) handler 來(lái)處理。

3.1.4. HttpObjectAggregator

  1. A ChannelHandler that aggregates an HttpMessage and its following HttpContent into a single FullHttpRequest or FullHttpResponse 
  2.  
  3. (depending on if it used to handle requests or responses) with no following HttpContent. 
  4.  
  5. It is useful when you don't want to take care of HTTP messages whose transfer encoding is 'chunked'. 

即通過(guò)它可以把 HttpMessage 和 HttpContent 聚合成一個(gè) FullHttpRequest 或者 FullHttpResponse (取決于是處理請(qǐng)求還是響應(yīng)),而且它還可以幫助你在解碼時(shí)忽略是否為“塊”傳輸方式。

因此,在解析 HTTP POST 請(qǐng)求時(shí),請(qǐng)務(wù)必在 ChannelPipeline 中加上 HttpObjectAggregator。(具體細(xì)節(jié)請(qǐng)自行查閱代碼)

當(dāng)然,netty 還提供了其他 HTTP 編解碼器,有些涉及到高級(jí)應(yīng)用(較復(fù)雜的應(yīng)用),在此就不一一解釋了,以上只是介紹netty HTTP 協(xié)議棧最基本的編解碼器(切合文章主題——淺析)。

3.2. HTTP GET 解析實(shí)踐

上面提到過(guò),HTTP GET 請(qǐng)求的參數(shù)是包含在 uri 中的,可通過(guò)以下方式解析出 uri:

  1. HttpRequest request = (HttpRequest) msg; 
  2.  
  3. String uri = request.uri(); 

特別注意的是,用瀏覽器發(fā)起 HTTP 請(qǐng)求時(shí),常常會(huì)被 uri = "/favicon.ico" 所干擾,因此最好對(duì)其特殊處理:

  1. if(uri.equals(FAVICON_ICO)){ 
  2.  
  3. return
  4.  

接下來(lái)就是解析 uri 了。這里需要用到 QueryStringDecoder:

  1. Splits an HTTP query string into a path string and key-value parameter pairs. 
  2.  This decoder is for one time use only.  Create a new instance for each URI: 
  3.   
  4.  QueryStringDecoder decoder = new QueryStringDecoder("/hello?recipient=world&x=1;y=2"); 
  5.  assert decoder.getPath().equals("/hello"); 
  6.  assert decoder.getParameters().get("recipient").get(0).equals("world"); 
  7.  assert decoder.getParameters().get("x").get(0).equals("1"); 
  8.  assert decoder.getParameters().get("y").get(0).equals("2"); 
  9.  
  10.  This decoder can also decode the content of an HTTP POST request whose 
  11.  content type is application/x-www-form-urlencoded: 
  12.  
  13.  QueryStringDecoder decoder = new QueryStringDecoder("recipient=world&x=1;y=2"false); 
  14.  ... 

從上面的描述可以看出,QueryStringDecoder 的作用就是把 HTTP uri 分割成 path 和 key-value 參數(shù)對(duì),也可以用來(lái)解碼 Content-Type = "application/x-www-form-urlencoded" 的 HTTP POST。特別注意的是,該 decoder 僅能使用一次。

解析代碼如下:

  1. String uri = request.uri(); 
  2. HttpMethod method = request.method(); 
  3. if(method.equals(HttpMethod.GET)){ 
  4.   QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8)); 
  5.   Map<String, List<String>> uriAttributes = queryDecoder.parameters(); 
  6.   //此處僅打印請(qǐng)求參數(shù)(你可以根據(jù)業(yè)務(wù)需求自定義處理) 
  7.   for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) { 
  8.     for (String attrVal : attr.getValue()) { 
  9.       System.out.println(attr.getKey() + "=" + attrVal); 
  10.     } 
  11.   } 

3.3. HTTP POST 解析實(shí)踐

如3.1.4小結(jié)所說(shuō)的那樣,解析 HTTP POST 請(qǐng)求的 message body,一定要使用 HttpObjectAggregator。但是,是否一定要把 msg 轉(zhuǎn)換成 FullHttpRequest 呢?答案是否定的,且往下看。

首先解釋下 FullHttpRequest 是什么:

  1. Combinate the HttpRequest and FullHttpMessage, so the request is a complete HTTP request. 

即 FullHttpRequest 包含了 HttpRequest 和 FullHttpMessage,是一個(gè) HTTP 請(qǐng)求的完全體。

而把 msg 轉(zhuǎn)換成 FullHttpRequest 的方法很簡(jiǎn)單:

  1. FullHttpRequest fullRequest = (FullHttpRequest) msg; 

接下來(lái)就是分幾種 Content-Type 進(jìn)行解析了。

3.3.1. 解析 application/json

處理 JSON 格式是非常方便的,我們只需要將 msg 轉(zhuǎn)換成 FullHttpRequest,然后將其 content 反序列化成 JSONObject 對(duì)象即可,如下:

  1. FullHttpRequest fullRequest = (FullHttpRequest) msg; 
  2.  
  3. String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8)); 
  4.  
  5. JSONObject obj = JSON.parseObject(jsonStr); 
  6.  
  7. for(Entry<String, Object> item : obj.entrySet()){ 
  8.  
  9. System.out.println(item.getKey()+"="+item.getValue().toString()); 
  10.  

3.3.2. 解析 application/x-www-form-urlencoded

解析此類型有兩種方法,一種是使用 QueryStringDecoder,另外一種就是使用 HttpPostRequestDecoder。

方法一:3.2節(jié)中講 QueryStringDecoder 時(shí)提到:QueryStringDecoder 可以用來(lái)解碼 Content-Type = "application/x-www-form-urlencoded" 的 HTTP POST。因此我們可以用它來(lái)解析 message body,剩下的處理就跟 HTTP GET沒(méi)什么兩樣了:

  1. FullHttpRequest fullRequest = (FullHttpRequest) msg; 
  2.  
  3. String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8)); 
  4.  
  5. QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false); 
  6.  
  7. Map<String, List<String>> uriAttributes = queryDecoder.parameters(); 
  8.  
  9. for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) { 
  10.  
  11. for (String attrVal : attr.getValue()) { 
  12.  
  13. System.out.println(attr.getKey()+"="+attrVal); 
  14.  
  15.  

方法二:使用 HttpPostRequestDecoder 解析時(shí),無(wú)需先將 msg 轉(zhuǎn)換成 FullHttpRequest。

我們先來(lái)了解下 HttpPostRequestDecoder :

  1. public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { 
  2.   if (factory == null) { 
  3.     throw new NullPointerException("factory"); 
  4.   } 
  5.   if (request == null) { 
  6.     throw new NullPointerException("request"); 
  7.   } 
  8.   if (charset == null) { 
  9.     throw new NullPointerException("charset"); 
  10.   } 
  11.   // Fill default values 
  12.   if (isMultipart(request)) { 
  13.     decoder = new HttpPostMultipartRequestDecoder(factory, request, charset); 
  14.   } else { 
  15.     decoder = new HttpPostStandardRequestDecoder(factory, request, charset); 
  16.   } 

由它的定義可知,它的內(nèi)部實(shí)現(xiàn)其實(shí)有兩種方式,一種是針對(duì) multipart 類型的解析,一種是普通類型的解析。這兩種方式的具體實(shí)現(xiàn)中,我把它們相同的代碼提取出來(lái),如下:

  1. if (request instanceof HttpContent) { 
  2.  
  3. // Offer automatically if the given request is als type of HttpContent 
  4.  
  5. offer((HttpContent) request); 
  6.  
  7. else { 
  8.  
  9. undecodedChunk = buffer(); 
  10.  
  11. parseBody(); 
  12.  

由于我們使用過(guò) HttpObjectAggregator, request 都是 HttpContent 類型,因此會(huì) Offer automatically,我們就不必自己手動(dòng)去 offer 了,也不用處理 Chunk,所以使用 HttpObjectAggregator 確實(shí)是帶來(lái)了很多簡(jiǎn)便的。

好了,接下來(lái)就是使用 HttpPostRequestDecoder 來(lái)解析了,直接上代碼:

  1. HttpRequest request = (HttpRequest) msg; 
  2.  
  3. HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request, Charsets.toCharset(CharEncoding.UTF_8)); 
  4.  
  5. List<InterfaceHttpData> datas = decoder.getBodyHttpDatas(); 
  6.  
  7. for (InterfaceHttpData data : datas) { 
  8.  
  9. if(data.getHttpDataType() == HttpDataType.Attribute) { 
  10.  
  11. Attribute attribute = (Attribute) data; 
  12.  
  13. System.out.println(attribute.getName() + "=" + attribute.getValue()); 
  14.  
  15.  

是不是很簡(jiǎn)單?沒(méi)錯(cuò)。但是這里有點(diǎn)我要說(shuō)明下, InterfaceHttpData 是一個(gè)interface,沒(méi)有 API 可以直接拿到它的 value。那怎么辦呢?莫方,在它的類內(nèi)部定義了個(gè)枚舉類型,如下:

  1. enum HttpDataType { 
  2.  
  3. Attribute, FileUpload, InternalAttribute 
  4.  

這種情況下它是 Attribute 類型,因此你轉(zhuǎn)換一下就能拿到值了。好奇的你可能會(huì)問(wèn),除 Attribute 外,其他兩個(gè)是什么時(shí)候用呢?沒(méi)錯(cuò),接下來(lái)馬上就講 FileUpload,至于 InternalAttribute 嘛,老夫就不多說(shuō)啦,有興趣可以自己去研究了哈~

3.3.3. 解析 multipart/form-data (文件上傳)

上面說(shuō)到了 FileUpload,那在這里就來(lái)說(shuō)說(shuō)如何使用 netty HTTP 協(xié)議棧實(shí)現(xiàn)文件上傳和保存功能。

這里依然使用 HttpPostRequestDecoder,廢話就不多少了,直接上代碼:

  1.  DiskFileUpload.baseDirectory = "/data/fileupload/"
  2.  
  3. HttpRequest request = (HttpRequest) msg; 
  4.  
  5. HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request, Charsets.toCharset(CharEncoding.UTF_8)); 
  6.  
  7. List<InterfaceHttpData> datas = decoder.getBodyHttpDatas(); 
  8.  
  9. for (InterfaceHttpData data : datas) { 
  10.  
  11. if(data.getHttpDataType() == HttpDataType.FileUpload) { 
  12.  
  13. FileUpload fileUpload = (FileUpload) data; 
  14.  
  15. String fileName = fileUpload.getFilename(); 
  16.  
  17. if(fileUpload.isCompleted()) { 
  18.  
  19. //保存到磁盤 
  20.  
  21. StringBuffer fileNameBuf = new StringBuffer(); 
  22.  
  23. fileNameBuf.append(DiskFileUpload.baseDirectory).append(fileName); 
  24.  
  25. fileUpload.renameTo(new File(fileNameBuf.toString())); 
  26.  
  27.  
  28. }} 

至于效果,你可以直接在本地起個(gè)服務(wù)搞個(gè)簡(jiǎn)單的頁(yè)面,向服務(wù)器傳個(gè)文件就行了。如果你很懶,直接用下面的HTML代碼改改將就著用吧:

  1. <form action="http://localhost:8080" method="post" enctype ="multipart/form-data" 
  2. <input id="File1" runat="server" name="UpLoadFile" type="file" />  
  3. <input type="submit" name="Button" value="上傳" id="Button" />  
  4. </form> 

至于其他類型的 Method、其他類型的 Content-Type,我也不打算細(xì)無(wú)巨細(xì)一一給大伙兒詳細(xì)講解了,看看上面羅列的,其實(shí)都很簡(jiǎn)單是不是?

上面說(shuō)的都是 netty 自己實(shí)現(xiàn)的東西,下面就來(lái)講講如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 HTTP decoder。

4. 自定義 HTTP POST 的 message body 解碼器

關(guān)于解碼器,我也不打算實(shí)現(xiàn)很復(fù)雜很牛逼的,只是寫了兩個(gè)粗糙的 decoder,一個(gè)是帶參數(shù)的一個(gè)是不帶參數(shù)的。既然是淺析,那就下面就簡(jiǎn)單的聊聊。

如果你要實(shí)現(xiàn)一個(gè)頂層解碼器,就要繼承 MessageToMessageDecoder 并重寫其 decode 方法。MessageToMessageDecoder 繼承了 ChannelHandlerAdapter,也就是說(shuō)解碼器其實(shí)就是一個(gè) handler,只不過(guò)是專門用來(lái)做解碼的事情。下面我們來(lái)看看它重寫的 channelRead 方法:

  1. @Override  
  2. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
  3. RecyclableArrayList out = RecyclableArrayList.newInstance();  
  4. try {  
  5.  
  6. if (acceptInboundMessage(msg)) {  
  7. @SuppressWarnings("unchecked" 
  8. cast = (I) msg;  
  9. try {  
  10. decode(ctx, castout);  
  11. } finally {  
  12. ReferenceCountUtil.release(cast);  
  13.  
  14. else {  
  15. out.add(msg);  
  16.  
  17. } catch (DecoderException e) {  
  18. throw e; 
  19.  
  20. } catch (Exception e) { 
  21.  
  22. throw new DecoderException(e); 
  23.  
  24. } finally { 
  25.  
  26. int size = out.size(); 
  27.  
  28. for (int i = 0; i < size; i ++) { 
  29.  
  30. ctx.fireChannelRead(out.get(i)); 
  31.  
  32.  
  33. out.recycle(); 
  34.  
  35.  

其中 decode 方法是你實(shí)現(xiàn) decoder 時(shí)需要重寫的,經(jīng)過(guò)解碼之后,會(huì)調(diào)用 ctx.fireChannelRead() 將 out 傳遞給給下一個(gè) handler 執(zhí)行相關(guān)邏輯。

4.1. HttpJsonDecoder

從名字可以看出,這是個(gè)針對(duì) message body 為 JsonString 的解碼器。處理過(guò)程很簡(jiǎn)單,只需要把 HTTP 請(qǐng)求的 content (即 ByteBuf)的可讀字節(jié)轉(zhuǎn)換成 JSONObject 對(duì)象,如下:

  1. @Override 
  2.  
  3. protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception { 
  4.  
  5. FullHttpRequest fullRequest = (FullHttpRequest) msg; 
  6.  
  7. ByteBuf content = fullRequest.content(); 
  8.  
  9. int length = content.readableBytes(); 
  10.  
  11. byte[] bytes = new byte[length]; 
  12.  
  13. for(int i=0; i<length; i++){ 
  14.  
  15. bytes[i] = content.getByte(i); 
  16.  
  17.  
  18. try{ 
  19.  
  20. JSONObject obj = JSON.parseObject(new String(bytes)); 
  21.  
  22. out.add(obj); 
  23.  
  24. }catch(ClassCastException e){ 
  25.  
  26. throw new CodecException("HTTP message body is not a JSONObject"); 
  27.  
  28.  

使用方法也很簡(jiǎn)單,在 Server 的 HttpServerCodec() 和 HttpObjectAggregator() 后面加上:

  1. .addLast("jsonDecoder", new HttpJsonDecoder()) 

然后在業(yè)務(wù) handler channelRead方法中使用即可:

  1. if(msg instanceof JSONObject){ 
  2.  
  3. JSONObject obj = (JSONObject) msg; 
  4.  
  5. ...... 
  6.  

4.2. HttpProtobufDecoder

這是一個(gè)帶參數(shù)的 decoder,用來(lái)解析使用 protobuf 序列化后的 message body。使用的時(shí)候需要傳遞 MessageLite 進(jìn)來(lái),直接上代碼:

  1. private final MessageLite prototype; 
  2.  
  3. public HttpProtobufDecoder(MessageLite prototype){ 
  4.  
  5. if (prototype == null) { 
  6.  
  7. throw new NullPointerException("prototype"); 
  8.  
  9.  
  10. this.prototype = prototype.getDefaultInstanceForType(); 
  11.  
  12.  
  13. @Override 
  14.  
  15. protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) { 
  16.  
  17. FullHttpRequest fullRequest = (FullHttpRequest) msg; 
  18.  
  19. ByteBuf content = fullRequest.content(); 
  20.  
  21. int length = content.readableBytes(); 
  22.  
  23. byte[] bytes = new byte[length]; 
  24.  
  25. for(int i=0; i<length; i++){ 
  26.  
  27. bytes[i] = content.getByte(i); 
  28.  
  29.  
  30. try { 
  31.  
  32. out.add(prototype.getParserForType().parseFrom(bytes, 0, length)); 
  33.  
  34. } catch (InvalidProtocolBufferException e) { 
  35.  
  36. throw new CodecException("HTTP message body is not " + prototype + "type"); 
  37.  
  38.  

使用方法跟 HttpJsonDecoder無(wú)異。此處以 protobuf 對(duì)象 UserProtobuf.User 為例,在 Server 的 HttpServerCodec() 和 HttpObjectAggregator() 后面加上:

  1. addLast("protobufDecoder", new HttpProtobufDecoder(UserProbuf.User.getDefaultInstance())) 

然后在業(yè)務(wù) handler channelRead方法中使用即可:

  1. if(msg instanceof UserProbuf.User){ 
  2.  
  3. UserProbuf.User user = (UserProbuf.User) msg; 
  4.  
  5. ...... 
  6.  

5. 聊聊開(kāi)發(fā)中遇到的問(wèn)題【推薦】

如果你沒(méi)有親自使用過(guò) netty 卻說(shuō)自己熟悉甚至精通 netty,我勸你千萬(wàn)別這么做,因?yàn)槟愕哪槙?huì)被打腫的。netty 作為一個(gè)異步非阻塞的 IO 框架,它到底多牛逼在這就不多扯了,而作為一個(gè)首次使用 netty HTTP 協(xié)議棧的我來(lái)說(shuō),踩坑是必不可少的過(guò)程。當(dāng)然了,踩了坑就要填上,我還很樂(lè)意在這把我踩過(guò)的幾個(gè)坑給大家分享下,前車之鑒。

5.1. 關(guān)于內(nèi)存泄漏

首先說(shuō)下經(jīng)歷的情況。在文章開(kāi)篇提到的接收服務(wù),經(jīng)過(guò)多輪的單元測(cè)試幾乎沒(méi)發(fā)現(xiàn)什么問(wèn)題,于是對(duì)于接下來(lái)的壓力測(cè)試我是自信滿滿。然而,當(dāng)我第一次跑壓測(cè)時(shí)就拋出一個(gè)異常,如下:

  1. [ERROR] 2016-07-24 15:25:46 [io.netty.util.internal.logging.Slf4JLogger:176] - LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel() See http://netty.io/wiki/reference-counted-objects.html for more information. 

著實(shí)讓我開(kāi)心了一把,終于出現(xiàn)異常了!異常信息表達(dá)的是 “ByteBuf 在被 JVM GC 之前沒(méi)有調(diào)用 ByteBuf.release() ,啟用高級(jí)泄漏報(bào)告,找出發(fā)生泄漏的地方”,于是馬上google了一把,原來(lái)是從 netty4 開(kāi)始,對(duì)象的生命周期由它們的引用計(jì)數(shù)(reference counts)管理,而不是由垃圾收集器(garbage collector)管理了。

要解決這個(gè)問(wèn)題,先從源頭了解開(kāi)始。

5.1.1. netty 引用計(jì)數(shù)對(duì)象【2】

對(duì)于 netty Inbound message,當(dāng) event loop 讀入了數(shù)據(jù)并創(chuàng)建了 ByteBuf,并用這個(gè) ByteBuf 觸發(fā)了一個(gè) channelRead() 事件時(shí),那么管道(pipeline)中相應(yīng)的ChannelHandler 就負(fù)責(zé)釋放這個(gè) buffer 。因此,處理接數(shù)據(jù)的 handler 應(yīng)該在它的 channelRead() 中調(diào)用 buffer 的 release(),如下:

  1. public void channelRead(ChannelHandlerContext ctx, Object msg) { 
  2.  
  3. ByteBuf buf = (ByteBuf) msg; 
  4.  
  5. try { 
  6.  
  7. ... 
  8.  
  9. } finally { 
  10.  
  11. buf.release(); 
  12.  
  13.  

而有時(shí)候,ByteBuf 會(huì)被一個(gè) buffer holder 持有,它們都擴(kuò)展了一個(gè)公共接口 ByteBufHolder。正因如此, ByteBuf 并不是 netty 中唯一一種引用計(jì)數(shù)對(duì)象。由 decoder 生成的消息對(duì)象很可能也是引用計(jì)數(shù)對(duì)象,比如 HTTP 協(xié)議棧中的 HttpContent,因?yàn)樗矓U(kuò)展了 ByteBufHolder。 

  1. public void channelRead(ChannelHandlerContext ctx, Object msg) { 
  2.  
  3. if (msg instanceof HttpRequest) { 
  4.  
  5. HttpRequest req = (HttpRequest) msg; 
  6.  
  7. ... 
  8.  
  9.  
  10. if (msg instanceof HttpContent) { 
  11.  
  12. HttpContent content = (HttpContent) msg; 
  13.  
  14. try { 
  15.  
  16. ... 
  17.  
  18. } finally { 
  19.  
  20. content.release(); 
  21.  
  22.  
  23.  

如果你抱有疑問(wèn),或者你想簡(jiǎn)化這些釋放消息的工作,你可以使用 ReferenceCountUtil.release():

  1. public void channelRead(ChannelHandlerContext ctx, Object msg) { 
  2.  
  3. try { 
  4.  
  5. ... 
  6.  
  7. } finally { 
  8.  
  9. ReferenceCountUtil.release(msg); 
  10.  
  11.  

或者可以考慮繼承 SimpleChannelHandler,它在所有接收消息的地方都調(diào)用了 ReferenceCountUtil.release(msg)。

對(duì)于 netty Outbound message,你的程序所創(chuàng)建的消息對(duì)象都由 netty 負(fù)責(zé)釋放,釋放的時(shí)機(jī)是在這些消息被發(fā)送到網(wǎng)絡(luò)之后。但是,在發(fā)送消息的過(guò)程中,如果有 handler 截獲(intercept)了你的發(fā)送請(qǐng)求并創(chuàng)建了一些中間對(duì)象,則這些 handler 要確保正確釋放這些中間對(duì)象。比如 encoder,此處不贅述。

通過(guò)以上信息,自然就很容易找到 OOM 問(wèn)題的原因所在了。由于在處理 HTTP 請(qǐng)求過(guò)程中沒(méi)有釋放 ByteBuf,因此在代碼 finally 塊中加上 ReferenceCountUtil.release(msg) 就解決啦!

5.1.2. 如何規(guī)避內(nèi)存泄漏【3】

netty 提供了內(nèi)存泄漏的監(jiān)測(cè)機(jī)制,默認(rèn)就會(huì)從分配的 ByteBuf 里抽樣出大約 1% 的來(lái)進(jìn)行跟蹤。如果泄漏,就會(huì)打印5.1.1節(jié)中的異常信息,并提示你通過(guò)指定 JVM 選項(xiàng)

  1. -Dio.netty.leakDetectionLevel=advanced 

來(lái)查看泄漏報(bào)告。泄漏年監(jiān)測(cè)有4個(gè)等級(jí):

  • 禁用(DISABLED) - 完全禁止泄露檢測(cè),省點(diǎn)消耗。
  • 簡(jiǎn)單(SIMPLE) - 默認(rèn)等級(jí),告訴我們?nèi)拥?1% 的 ByteBuf 是否發(fā)生了泄露,但總共一次只打印一次,看不到就沒(méi)有了。
  • 高級(jí)(ADVANCED) - 告訴我們?nèi)拥?1% 的 ByteBuf 發(fā)生泄露的地方。每種類型的泄漏(創(chuàng)建的地方與訪問(wèn)路徑一致)只打印一次。
  • 偏執(zhí)(PARANOID) - 跟高級(jí)選項(xiàng)類似,但此選項(xiàng)檢測(cè)所有 ByteBuf,而不僅僅是取樣的那 1%。在高壓力測(cè)試時(shí),對(duì)性能有明顯影響。

一般情況下我們采用 SIMPLE 級(jí)別即可。

5.2. 關(guān)于 HTTP 長(zhǎng)連接

按照慣例,先說(shuō)下開(kāi)發(fā)中踩到的坑。

對(duì)于接收服務(wù),我采用的是 nginx + netty http,其中 nginx 配置如下(閹割隱藏版):

  1. upstream xxx.com{ 
  2.  
  3. keepalive 32; 
  4.  
  5. server xxxx.xx.xx.xx:8080; 
  6.  
  7.  
  8. server{ 
  9.  
  10. listen 80; 
  11.  
  12. server_name xxx.com; 
  13.  
  14. location / { 
  15.  
  16. proxy_next_upstream http_502 http_504 error timeout invalid_header; 
  17.  
  18. proxy_pass xxx.com; 
  19.  
  20. proxy_http_version 1.1; 
  21.  
  22. proxy_set_header Connection ""
  23.  
  24. #proxy_set_header Host $host; 
  25.  
  26. #proxy_set_header X-Forwarded-For $remote_addr; 
  27.  
  28. #proxy_set_header REMOTE_ADDR $remote_addr; 
  29.  
  30. #proxy_set_header X-Real-IP $remote_addr; 
  31.  
  32. proxy_read_timeout 60s; 
  33.  
  34. client_max_body_size 1m; 
  35.  
  36.  
  37. error_page 500 502 503 504 /50x.html; 
  38.  
  39. location = /50x.html{ 
  40.  
  41. root html; 
  42.  
  43.  

然后編寫了一個(gè)簡(jiǎn)單的 HttpClient 發(fā)送消息,如下(截取):

  1. OutputStream outStream = conn.getOutputStream(); 
  2. outStream.write(data); 
  3. outStream.flush(); 
  4. outStream.close(); 
  5.               
  6. if (conn.getResponseCode() == 200) { 
  7.   <span style="color: #ff0000;">BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8"));</span> 
  8.   String msg = in.readLine(); 
  9.   System.out.println("msg = " + msg); 
  10.   in.close(); 
  11. conn.disconnect(); 

接著,正常發(fā)送 HTTP 請(qǐng)求到服務(wù)器,然而,老夫整整等了60多秒才接到響應(yīng)信息!而且每次都這樣!!

我首先懷疑是不是 ngxin 出問(wèn)題了,有一個(gè)配置項(xiàng)立馬引起了我的懷疑,沒(méi)錯(cuò),就是上面紅色的那行 proxy_read_timeout 60s; 。為了驗(yàn)證,我首先把 60s 改成了 10s,效果很明顯,發(fā)送的請(qǐng)求 10 秒過(guò)一點(diǎn)就收到響應(yīng)了!更加徹底證明是 nginx 的鍋,我去掉了 nginx,讓客戶端直接發(fā)送請(qǐng)求給服務(wù)端。然而,蛋疼的事情出現(xiàn)了,客戶端竟然一直阻塞在 BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8")); 處。這說(shuō)明根本就不是 nginx 的問(wèn)題啊!

我冷靜下來(lái),review 了一下代碼同時(shí) search 了相關(guān)資料,發(fā)現(xiàn)了一個(gè)小小的區(qū)別,在我的返回代碼中,對(duì) ChannelFuture 少了對(duì) CLOSE 事件的監(jiān)聽(tīng)器:

  1. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 

于是,我加上 Listener 再試一下,馬上就得到響應(yīng)了!

就在這一刻明白了這是 HTTP 長(zhǎng)連接的問(wèn)題。首先從上面的 nginx 配置中可以看到,我顯式指定了 nginx 和 HTTP 服務(wù)器是用的 HTTP1.1 版本,HTTP1.1 版本默認(rèn)是長(zhǎng)連接方式(也就是 Connection=Keep-Alive),而我在 netty HTTP 服務(wù)器中并沒(méi)有對(duì)長(zhǎng)、短連接方式做區(qū)別處理,并且在 HttpResponse 響應(yīng)中并沒(méi)有顯式加上 Content-Length 頭部信息,恰巧 netty Http 協(xié)議棧并沒(méi)有在框架上做這件工作,導(dǎo)致服務(wù)端雖然把響應(yīng)消息發(fā)出去了,但是客戶端并不知道你是否發(fā)送完成了(即沒(méi)辦法判斷數(shù)據(jù)是否已經(jīng)發(fā)送完)。

于是,把響應(yīng)的處理完善一下即可: 

  1. /** 
  2.  
  3. * 響應(yīng)報(bào)文處理 
  4.  
  5. * @param channel 當(dāng)前上下文Channel 
  6.  
  7. * @param status 響應(yīng)碼 
  8.  
  9. * @param msg 響應(yīng)消息 
  10.  
  11. * @param forceClose 是否強(qiáng)制關(guān)閉 
  12.  
  13. */ 
  14.  
  15. private void writeResponse(Channel channel, HttpResponseStatus status, String msg, boolean forceClose){ 
  16.  
  17. ByteBuf byteBuf = Unpooled.wrappedBuffer(msg.getBytes()); 
  18.  
  19. response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, byteBuf); 
  20.  
  21. boolean close = isClose(); 
  22.  
  23. if(!close && !forceClose){ 
  24.  
  25. response.headers().add(org.apache.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(byteBuf.readableBytes())); 
  26.  
  27.  
  28. ChannelFuture future = channel.write(response); 
  29.  
  30. if(close || forceClose){ 
  31.  
  32. future.addListener(ChannelFutureListener.CLOSE); 
  33.  
  34.  
  35.  
  36. private boolean isClose(){ 
  37.  
  38. if(request.headers().contains(org.apache.http.HttpHeaders.CONNECTION, CONNECTION_CLOSE, true) || 
  39.  
  40. (request.protocolVersion().equals(HttpVersion.HTTP_1_0) && 
  41.  
  42. !request.headers().contains(org.apache.http.HttpHeaders.CONNECTION, CONNECTION_KEEP_ALIVE, true))) 
  43.  
  44. return true
  45.  
  46. return false
  47.  

好了,問(wèn)題是解決了,那么你對(duì) HTTP 長(zhǎng)連接真的了解嗎?不了解,好,那就來(lái)不補(bǔ)課。

5.2.1. TCP KeepAlive 和 HTTP KeepAlive【4】

netty 中有個(gè)地方比較讓初學(xué)者迷惑,就是 childOption(ChannelOption.SO_KEEPALIVE, true)和 HttpRequest.Headers.get("Connection").equals("Keep-Alive") (非標(biāo)準(zhǔn)寫法,僅作示例)的異同。有些人可能會(huì)問(wèn),我在 ServerBootstrap 中指定了 childOption(ChannelOption.SO_KEEPALIVE, true),是不是就意味著客戶端和服務(wù)器是長(zhǎng)連接了?

答案當(dāng)然不是。

首先,TCP 的 KeepAlive 是 TCP 連接的探測(cè)機(jī)制,用來(lái)檢測(cè)當(dāng)前 TCP 連接是否活著。它支持三個(gè)系統(tǒng)內(nèi)核參數(shù)

  • tcp_keepalive_time
  • tcp_keepalive_intvl
  • tcp_keepalive_probes

當(dāng)網(wǎng)絡(luò)兩端建立了 TCP 連接之后,閑置 idle(雙方?jīng)]有任何數(shù)據(jù)流發(fā)送往來(lái))了 tcp_keepalive_time 后,服務(wù)器內(nèi)核就會(huì)嘗試向客戶端發(fā)送偵測(cè)包,來(lái)判斷 TCP 連接狀況(有可能客戶端崩潰、強(qiáng)制關(guān)閉了應(yīng)用、主機(jī)不可達(dá)等等)。如果沒(méi)有收到對(duì)方的回答( ACK 包),則會(huì)在 tcp_keepalive_intvl 后再次嘗試發(fā)送偵測(cè)包,直到收到對(duì)對(duì)方的 ACK,如果一直沒(méi)有收到對(duì)方的 ACK,一共會(huì)嘗試 tcp_keepalive_probes 次,每次的間隔時(shí)間在這里分別是 15s、30s、45s、60s、75s。如果嘗試 tcp_keepalive_probes,依然沒(méi)有收到對(duì)方的 ACK 包,則會(huì)丟棄該 TCP 連接。TCP 連接默認(rèn)閑置時(shí)間是2小時(shí)。

而對(duì)于 HTTP 的 KeepAlive,則是讓 TCP 連接活長(zhǎng)一點(diǎn),在一次 TCP 連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會(huì)斷開(kāi)連接。通過(guò)使用 keep-alive 機(jī)制,可以減少 TCP 連接建立次數(shù),也意味著可以減少 TIME_WAIT 狀態(tài)連接,以此提高性能和提高 TTTP 服務(wù)器的吞吐率(更少的 TCP 連接意味著更少的系統(tǒng)內(nèi)核調(diào)用,socket 的 accept() 和 close() 調(diào)用)。

對(duì)于建立 HTTP 長(zhǎng)連接的好處,總結(jié)如下【5】:

By opening and closing fewer TCP connections, CPU time is saved in routers and hosts (clients, servers, proxies, gateways, tunnels, or caches), and memory used for TCP protocol control blocks can be saved in hosts.

HTTP requests and responses can be pipelined on a connection. Pipelining allows a client to make multiple requests without waiting for each response, allowing a single TCP connection to be used much more efficiently, with much lower elapsed time.

Network congestion is reduced by reducing the number of packets caused by TCP opens, and by allowing TCP sufficient time to determine the congestion state of the network.

Latency on subsequent requests is reduced since there is no time spent in TCP's connection opening handshake.

HTTP can evolve more gracefully, since errors can be reported without the penalty of closing the TCP connection. Clients using future versions of HTTP might optimistically try a new feature, but if communicating with an older server, retry with old semantics after an error is reported.

5.2.2. 長(zhǎng)連接方式中如何判斷數(shù)據(jù)發(fā)送完成【6】

回到本節(jié)最開(kāi)始提出的問(wèn)題,KeepAlive 模式下,HTTP 服務(wù)器在發(fā)送完數(shù)據(jù)后并不會(huì)主動(dòng)斷開(kāi)連接,那客戶端如何判斷數(shù)據(jù)發(fā)送完成了?

對(duì)于短連接方式,服務(wù)端在發(fā)送完數(shù)據(jù)后會(huì)斷開(kāi)連接,客戶端過(guò)服務(wù)器關(guān)閉連接能確定消息的傳輸長(zhǎng)度。(請(qǐng)求端不能通過(guò)關(guān)閉連接來(lái)指明請(qǐng)求消息體的結(jié)束,因?yàn)檫@樣讓服務(wù)器沒(méi)有機(jī)會(huì)繼續(xù)給予響應(yīng))。

但對(duì)于長(zhǎng)連接方式,服務(wù)端只有在 Keep-alive timeout 或者達(dá)到 max 請(qǐng)求次數(shù)時(shí)才會(huì)斷開(kāi)連接。這種情況下有兩種判斷方法。

使用消息頭部 Content-Length

Conent-Length 表示實(shí)體內(nèi)容長(zhǎng)度,客戶端(或服務(wù)器)可以根據(jù)這個(gè)值來(lái)判斷數(shù)據(jù)是否接收完成。但是如果消息中沒(méi)有 Conent-Length,那該如何來(lái)判斷呢?又在什么情況下會(huì)沒(méi)有 Conent-Length 呢?

使用消息首部字段 Transfer-Encoding

當(dāng)請(qǐng)求或響應(yīng)的內(nèi)容是動(dòng)態(tài)的,客戶端或服務(wù)器無(wú)法預(yù)先知道要傳輸?shù)臄?shù)據(jù)大小時(shí),就要使用 Transfer-Encoding(即 chunked 編碼傳輸)。chunked 編碼將數(shù)據(jù)分成一塊一塊的發(fā)送。chunked 編碼將使用若干個(gè)chunk 串連而成,由一個(gè)標(biāo)明長(zhǎng)度為 0 的 chunk 標(biāo)示結(jié)束。每個(gè) chunk 分為頭部和正文兩部分,頭部?jī)?nèi)容指定正文的字符總數(shù)(十六進(jìn)制的數(shù)字)和數(shù)量單位(一般不寫),正文部分就是指定長(zhǎng)度的實(shí)際內(nèi)容,兩部分之間用回車換行(CRLF)隔開(kāi)。在最后一個(gè)長(zhǎng)度為 0 的 chunk 中的內(nèi)容是稱為footer的內(nèi)容,是一些附加的Header信息(通??梢灾苯雍雎?。

如果一個(gè)請(qǐng)求包含一個(gè)消息主體并且沒(méi)有給出 Content-Length,那么服務(wù)器如果不能判斷消息長(zhǎng)度的話應(yīng)該以400響應(yīng)(Bad Request),或者以411響應(yīng)(Length Required)如果它堅(jiān)持想要收到一個(gè)有效的 Content-length。所有的能接收實(shí)體的 HTTP/1.1 應(yīng)用程序必須能接受 chunked 的傳輸編碼,因此當(dāng)消息的長(zhǎng)度不能被提前確定時(shí),可以利用這種機(jī)制來(lái)處理消息。消息不能同時(shí)都包括 Content-Length 頭域和 非identity (Transfer-Encoding)傳輸編碼。如果消息包括了一個(gè) 非identity 的傳輸編碼,Content-Length頭域必須被忽略。當(dāng) Content-Length 頭域出現(xiàn)在一個(gè)具有消息主體(message-body)的消息里,它的域值必須精確匹配消息主體里字節(jié)數(shù)量。

好了,本章較長(zhǎng),雖然不是很深?yuàn)W難懂的知識(shí),也不是很牛逼的技術(shù)實(shí)現(xiàn),但是耐心看完之后相信你終究是有所收獲的。在此本文就要完結(jié)了,后續(xù)會(huì)對(duì) netty HTTP 協(xié)議棧做更深入的研究,至于這個(gè) github 上的項(xiàng)目,后面也會(huì)繼續(xù)完善 TODO LIST。大家可以通過(guò)多種方式與我交流,并歡迎大家提出寶貴意見(jiàn)。

責(zé)任編輯:武曉燕 來(lái)源: 博客園
相關(guān)推薦

2010-06-11 14:15:23

WAP協(xié)議棧

2010-09-08 17:40:56

協(xié)議棧是什么

2014-10-22 09:36:41

TCPIP

2009-08-03 13:12:34

ASP.NET編程模型

2019-08-23 06:36:32

2009-08-03 11:21:47

ASP.NET編程模型

2013-07-09 14:36:24

2014-11-13 10:57:03

http協(xié)議

2020-05-22 09:12:46

HTTP3網(wǎng)絡(luò)協(xié)議

2020-07-09 08:14:43

TCPIP協(xié)議棧

2010-08-02 16:43:46

ICMP協(xié)議

2019-04-23 10:48:55

HTTPTomcat服務(wù)器

2018-04-20 09:36:23

NettyWebSocket京東

2010-05-25 13:20:46

http與svn

2009-06-03 15:52:34

堆內(nèi)存棧內(nèi)存Java內(nèi)存分配

2009-08-11 14:51:11

C#數(shù)據(jù)結(jié)構(gòu)與算法

2021-05-18 10:32:40

Windows操作系統(tǒng)漏洞

2017-05-26 10:35:13

前端HTTP

2021-05-07 09:17:21

HTTPTCP協(xié)議

2013-05-08 12:42:39

HTTP協(xié)議IIS原理ASP.NET
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)