精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

netty5 HTTP協議棧淺析與實踐

開發 架構
HTTP 請求有很多種 method,最常用的就是 GET 和 POST,每種 method 的請求之間會有細微的區別。下面分別分析一下 GET 和 POST 請求。

 閱讀目錄

1. 寫在前面的話

1.1. 關于netty example

1.2. 關于github項目

2. HTTP 協議知多少

2.1. GET請求

2.2. POST請求

2.3. HTTP POST Content-Type

3. netty HTTP 編解碼

3.1. netty 自帶 HTTP 編解碼器

3.2. HTTP GET 解析實踐

3.3. HTTP POST 解析實踐

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

4.1. HttpJsonDecoder

4.2. HttpProtobufDecoder

5. 聊聊開發中遇到的問題【推薦】

5.1. 關于內存泄漏

5.1.1. netty 應用計數對象

5.1.2. 如何規避內存泄漏

5.2. 關于 HTTP 長連接

5.2.1. TCP KeepAlive 和 HTTP KeepAlive

5.2.2. 長連接方式中如何判斷數據發送完成

1. 說在前面的話

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

先用個“概念圖”來描繪下整個系統的架構:

嗯,這個是真正的“概念圖”,因為我已經把大部分細節都屏蔽了,別笑,因為本文的重點只是整個架構中的一小部分,就是上圖中紅框內的 http server。

也許你會問,這不就是個 HTTP 服務器嗎,而且是只處理一個請求的 HTTP 服務器,搞個java web 項目在 Tomcat 中一啟動不就完事兒了,有啥好講的呀?。莫慌,且聽老夫慢慢道來為啥要用 netty HTTP 協議棧來實現這個接收轉發服務。

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

基于以上幾點原因,老夫就決定使用 netty HTTP 協議棧開干啦~

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

1.1. 關于netty example

netty 官方提供了關于 HTTP 的例子,大伙兒可以在 netty 項目中查看。

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. 關于github項目

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

  • HTTP GET 請求解析與響應
  • HTTP POST 請求解析與響應,提供 application/json、application/x-www-form-urlencoded、multipart/form-data 三種常見 Content-Type 的 message body 解析示例
  • HTTP decoder實現,提供 POST 請求 message body 解碼器的 HttpJsonDecoder 及 HttpProtobufDecoder 實現示例
  • 作為服務端接收瀏覽器文件上傳及保存

將來可能會繼續實現的功能有:

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

如果你也打算使用 netty 來實現 HTTP 服務器,相信這個項目和本文對你是有較大幫助的!

好了,閑話不多說,下面正式進入正題。

2. HTTP 協議知多少

要通過 netty 實現 HTTP 服務端(或者客戶端),首先你得了解 HTTP 協議【1】。

HTTP 協議是請求/響應式的協議,客戶端需要發送一個請求,服務器才會返回響應內容。例如在瀏覽器上輸入一個網址按下 Enter,或者提交一個 Form 表單,瀏覽器就會發送一個請求到服務器,而打開的網頁的內容,就是服務器返回的響應。

下面講下 HTTP 請求和響應包含的內容。

HTTP 請求有很多種 method,最常用的就是 GET 和 POST,每種 method 的請求之間會有細微的區別。下面分別分析一下 GET 和 POST 請求。

2.1. GET請求

下面是瀏覽器對 http://localhost:8081/test?name=XXG&age=23 的 GET 請求時發送給服務器的數據:

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

2.2. POST請求

下面是瀏覽器對 http://localhost:8081/test 的 POST 請求時發送給服務器的數據,同樣帶上參數 name=XXG&age=23:

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

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

在上面的 HTTP 響應中,Header 中的 Content-Length 同樣用于表示 message body 的字節數。Content-Type 表示 message body 的類型,通常瀏覽網頁其類型是HTML,當然還會有其他類型,比如圖片、視頻等。

2.3. HTTP POST Content-Type

HTTP/1.1 協議規定的 HTTP 請求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。其中 POST 一般用來向服務端提交數據,本文討論主要的幾種 POST 提交數據方式。

我們知道,HTTP 協議是以 ASCII 碼傳輸,建立在 TCP/IP 協議之上的應用層規范。規范把 HTTP 請求分為三個部分:狀態行、請求頭、消息主體。類似于下面這樣:

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

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

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

2.3.1. application/x-www-form-urlencoded

這應該是最常見的 POST 提交數據的方式了。瀏覽器的原生 Form 表單,如果不設置 enctype 屬性,那么最終就會以 application/x-www-form-urlencoded 方式提交數據。請求類似于下面這樣(無關的請求頭在本文中都省略掉了):

  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;其次,提交的數據按照 key1=val1&key2=val2 的方式進行編碼,key 和 val 都進行了 URL 轉碼。大部分服務端語言都對這種方式有很好的支持。

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

2.3.2. multipart/form-data

這又是一個常見的 POST 數據提交的方式。我們使用表單上傳文件時,必須讓 Form 的 enctyped 等于這個值。直接來看一個請求示例:

  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-- 

這個例子稍微復雜點。首先生成了一個 boundary 用于分割不同的字段,為了避免與正文內容重復,boundary 很長很復雜。然后 Content-Type 里指明了數據是以 mutipart/form-data 來編碼,本次請求的 boundary 是什么內容。消息主體里按照字段個數又分為多個結構類似的部分,每部分都是以 –boundary 開始,緊接著內容描述信息,然后是回車,最后是字段具體內容(文本或二進制)。如果傳輸的是文件,還要包含文件名和文件類型信息。消息主體最后以 –boundary– 標示結束。

這種方式一般用來上傳文件,各大服務端語言對它也有著良好的支持。

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

2.3.3. application/json

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

JSON 格式支持比鍵值對復雜得多的結構化數據,這一點也很有用,當需要提交的數據層次非常深,就可以考慮把數據 JSON 序列化之后來提交的。

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

最終發送的請求是:

  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]} 

這種方案,可以方便的提交復雜的結構化數據,特別適合 RESTful 的接口。各大抓包工具如 Chrome 自帶的開發者工具、Fiddler,都會以樹形結構展示 JSON 數據,非常友好。

其他幾種 Content-Type 就不一一詳細介紹了,感興趣的童鞋請自行了解。下面進入 netty 支持 HTTP 協議的源碼分析階段。

3. netty HTTP 編解碼

要通過 netty 處理 HTTP 請求,需要先進行編解碼。

3.1. netty 自帶 HTTP 編解碼器

netty5 提供了對 HTTP 協議的幾種編解碼器:

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 的結合。

因此,基于 netty 實現 HTTP 服務端時,需要在 ChannelPipeline 中加上以上編解碼器:

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

或者

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

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

這種情況該怎么辦呢?別慌,netty 提供了一個 handler 來處理。

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'. 

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

因此,在解析 HTTP POST 請求時,請務必在 ChannelPipeline 中加上 HttpObjectAggregator。(具體細節請自行查閱代碼)

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

3.2. HTTP GET 解析實踐

上面提到過,HTTP GET 請求的參數是包含在 uri 中的,可通過以下方式解析出 uri:

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

特別注意的是,用瀏覽器發起 HTTP 請求時,常常會被 uri = "/favicon.ico" 所干擾,因此最好對其特殊處理:

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

接下來就是解析 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 參數對,也可以用來解碼 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.   //此處僅打印請求參數(你可以根據業務需求自定義處理) 
  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 解析實踐

如3.1.4小結所說的那樣,解析 HTTP POST 請求的 message body,一定要使用 HttpObjectAggregator。但是,是否一定要把 msg 轉換成 FullHttpRequest 呢?答案是否定的,且往下看。

首先解釋下 FullHttpRequest 是什么:

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

即 FullHttpRequest 包含了 HttpRequest 和 FullHttpMessage,是一個 HTTP 請求的完全體。

而把 msg 轉換成 FullHttpRequest 的方法很簡單:

  1. FullHttpRequest fullRequest = (FullHttpRequest) msg; 

接下來就是分幾種 Content-Type 進行解析了。

3.3.1. 解析 application/json

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

  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節中講 QueryStringDecoder 時提到:QueryStringDecoder 可以用來解碼 Content-Type = "application/x-www-form-urlencoded" 的 HTTP POST。因此我們可以用它來解析 message body,剩下的處理就跟 HTTP GET沒什么兩樣了:

  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 解析時,無需先將 msg 轉換成 FullHttpRequest。

我們先來了解下 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.   } 

由它的定義可知,它的內部實現其實有兩種方式,一種是針對 multipart 類型的解析,一種是普通類型的解析。這兩種方式的具體實現中,我把它們相同的代碼提取出來,如下:

  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.  

由于我們使用過 HttpObjectAggregator, request 都是 HttpContent 類型,因此會 Offer automatically,我們就不必自己手動去 offer 了,也不用處理 Chunk,所以使用 HttpObjectAggregator 確實是帶來了很多簡便的。

好了,接下來就是使用 HttpPostRequestDecoder 來解析了,直接上代碼:

  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.  

是不是很簡單?沒錯。但是這里有點我要說明下, InterfaceHttpData 是一個interface,沒有 API 可以直接拿到它的 value。那怎么辦呢?莫方,在它的類內部定義了個枚舉類型,如下:

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

這種情況下它是 Attribute 類型,因此你轉換一下就能拿到值了。好奇的你可能會問,除 Attribute 外,其他兩個是什么時候用呢?沒錯,接下來馬上就講 FileUpload,至于 InternalAttribute 嘛,老夫就不多說啦,有興趣可以自己去研究了哈~

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

上面說到了 FileUpload,那在這里就來說說如何使用 netty HTTP 協議棧實現文件上傳和保存功能。

這里依然使用 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. }} 

至于效果,你可以直接在本地起個服務搞個簡單的頁面,向服務器傳個文件就行了。如果你很懶,直接用下面的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,我也不打算細無巨細一一給大伙兒詳細講解了,看看上面羅列的,其實都很簡單是不是?

上面說的都是 netty 自己實現的東西,下面就來講講如何實現一個簡單的 HTTP decoder。

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

關于解碼器,我也不打算實現很復雜很牛逼的,只是寫了兩個粗糙的 decoder,一個是帶參數的一個是不帶參數的。既然是淺析,那就下面就簡單的聊聊。

如果你要實現一個頂層解碼器,就要繼承 MessageToMessageDecoder 并重寫其 decode 方法。MessageToMessageDecoder 繼承了 ChannelHandlerAdapter,也就是說解碼器其實就是一個 handler,只不過是專門用來做解碼的事情。下面我們來看看它重寫的 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 方法是你實現 decoder 時需要重寫的,經過解碼之后,會調用 ctx.fireChannelRead() 將 out 傳遞給給下一個 handler 執行相關邏輯。

4.1. HttpJsonDecoder

從名字可以看出,這是個針對 message body 為 JsonString 的解碼器。處理過程很簡單,只需要把 HTTP 請求的 content (即 ByteBuf)的可讀字節轉換成 JSONObject 對象,如下:

  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.  

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

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

然后在業務 handler channelRead方法中使用即可:

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

4.2. HttpProtobufDecoder

這是一個帶參數的 decoder,用來解析使用 protobuf 序列化后的 message body。使用的時候需要傳遞 MessageLite 進來,直接上代碼:

  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無異。此處以 protobuf 對象 UserProtobuf.User 為例,在 Server 的 HttpServerCodec() 和 HttpObjectAggregator() 后面加上:

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

然后在業務 handler channelRead方法中使用即可:

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

5. 聊聊開發中遇到的問題【推薦】

如果你沒有親自使用過 netty 卻說自己熟悉甚至精通 netty,我勸你千萬別這么做,因為你的臉會被打腫的。netty 作為一個異步非阻塞的 IO 框架,它到底多牛逼在這就不多扯了,而作為一個首次使用 netty HTTP 協議棧的我來說,踩坑是必不可少的過程。當然了,踩了坑就要填上,我還很樂意在這把我踩過的幾個坑給大家分享下,前車之鑒。

5.1. 關于內存泄漏

首先說下經歷的情況。在文章開篇提到的接收服務,經過多輪的單元測試幾乎沒發現什么問題,于是對于接下來的壓力測試我是自信滿滿。然而,當我第一次跑壓測時就拋出一個異常,如下:

  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. 

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

要解決這個問題,先從源頭了解開始。

5.1.1. netty 引用計數對象【2】

對于 netty Inbound message,當 event loop 讀入了數據并創建了 ByteBuf,并用這個 ByteBuf 觸發了一個 channelRead() 事件時,那么管道(pipeline)中相應的ChannelHandler 就負責釋放這個 buffer 。因此,處理接數據的 handler 應該在它的 channelRead() 中調用 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.  

而有時候,ByteBuf 會被一個 buffer holder 持有,它們都擴展了一個公共接口 ByteBufHolder。正因如此, ByteBuf 并不是 netty 中唯一一種引用計數對象。由 decoder 生成的消息對象很可能也是引用計數對象,比如 HTTP 協議棧中的 HttpContent,因為它也擴展了 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.  

如果你抱有疑問,或者你想簡化這些釋放消息的工作,你可以使用 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,它在所有接收消息的地方都調用了 ReferenceCountUtil.release(msg)。

對于 netty Outbound message,你的程序所創建的消息對象都由 netty 負責釋放,釋放的時機是在這些消息被發送到網絡之后。但是,在發送消息的過程中,如果有 handler 截獲(intercept)了你的發送請求并創建了一些中間對象,則這些 handler 要確保正確釋放這些中間對象。比如 encoder,此處不贅述。

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

5.1.2. 如何規避內存泄漏【3】

netty 提供了內存泄漏的監測機制,默認就會從分配的 ByteBuf 里抽樣出大約 1% 的來進行跟蹤。如果泄漏,就會打印5.1.1節中的異常信息,并提示你通過指定 JVM 選項

  1. -Dio.netty.leakDetectionLevel=advanced 

來查看泄漏報告。泄漏年監測有4個等級:

  • 禁用(DISABLED) - 完全禁止泄露檢測,省點消耗。
  • 簡單(SIMPLE) - 默認等級,告訴我們取樣的 1% 的 ByteBuf 是否發生了泄露,但總共一次只打印一次,看不到就沒有了。
  • 高級(ADVANCED) - 告訴我們取樣的 1% 的 ByteBuf 發生泄露的地方。每種類型的泄漏(創建的地方與訪問路徑一致)只打印一次。
  • 偏執(PARANOID) - 跟高級選項類似,但此選項檢測所有 ByteBuf,而不僅僅是取樣的那 1%。在高壓力測試時,對性能有明顯影響。

一般情況下我們采用 SIMPLE 級別即可。

5.2. 關于 HTTP 長連接

按照慣例,先說下開發中踩到的坑。

對于接收服務,我采用的是 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.  

然后編寫了一個簡單的 HttpClient 發送消息,如下(截取):

  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(); 

接著,正常發送 HTTP 請求到服務器,然而,老夫整整等了60多秒才接到響應信息!而且每次都這樣!!

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

我冷靜下來,review 了一下代碼同時 search 了相關資料,發現了一個小小的區別,在我的返回代碼中,對 ChannelFuture 少了對 CLOSE 事件的監聽器:

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

于是,我加上 Listener 再試一下,馬上就得到響應了!

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

于是,把響應的處理完善一下即可: 

  1. /** 
  2.  
  3. * 響應報文處理 
  4.  
  5. * @param channel 當前上下文Channel 
  6.  
  7. * @param status 響應碼 
  8.  
  9. * @param msg 響應消息 
  10.  
  11. * @param forceClose 是否強制關閉 
  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.  

好了,問題是解決了,那么你對 HTTP 長連接真的了解嗎?不了解,好,那就來不補課。

5.2.1. TCP KeepAlive 和 HTTP KeepAlive【4】

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

答案當然不是。

首先,TCP 的 KeepAlive 是 TCP 連接的探測機制,用來檢測當前 TCP 連接是否活著。它支持三個系統內核參數

  • tcp_keepalive_time
  • tcp_keepalive_intvl
  • tcp_keepalive_probes

當網絡兩端建立了 TCP 連接之后,閑置 idle(雙方沒有任何數據流發送往來)了 tcp_keepalive_time 后,服務器內核就會嘗試向客戶端發送偵測包,來判斷 TCP 連接狀況(有可能客戶端崩潰、強制關閉了應用、主機不可達等等)。如果沒有收到對方的回答( ACK 包),則會在 tcp_keepalive_intvl 后再次嘗試發送偵測包,直到收到對對方的 ACK,如果一直沒有收到對方的 ACK,一共會嘗試 tcp_keepalive_probes 次,每次的間隔時間在這里分別是 15s、30s、45s、60s、75s。如果嘗試 tcp_keepalive_probes,依然沒有收到對方的 ACK 包,則會丟棄該 TCP 連接。TCP 連接默認閑置時間是2小時。

而對于 HTTP 的 KeepAlive,則是讓 TCP 連接活長一點,在一次 TCP 連接中可以持續發送多份數據而不會斷開連接。通過使用 keep-alive 機制,可以減少 TCP 連接建立次數,也意味著可以減少 TIME_WAIT 狀態連接,以此提高性能和提高 TTTP 服務器的吞吐率(更少的 TCP 連接意味著更少的系統內核調用,socket 的 accept() 和 close() 調用)。

對于建立 HTTP 長連接的好處,總結如下【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. 長連接方式中如何判斷數據發送完成【6】

回到本節最開始提出的問題,KeepAlive 模式下,HTTP 服務器在發送完數據后并不會主動斷開連接,那客戶端如何判斷數據發送完成了?

對于短連接方式,服務端在發送完數據后會斷開連接,客戶端過服務器關閉連接能確定消息的傳輸長度。(請求端不能通過關閉連接來指明請求消息體的結束,因為這樣讓服務器沒有機會繼續給予響應)。

但對于長連接方式,服務端只有在 Keep-alive timeout 或者達到 max 請求次數時才會斷開連接。這種情況下有兩種判斷方法。

使用消息頭部 Content-Length

Conent-Length 表示實體內容長度,客戶端(或服務器)可以根據這個值來判斷數據是否接收完成。但是如果消息中沒有 Conent-Length,那該如何來判斷呢?又在什么情況下會沒有 Conent-Length 呢?

使用消息首部字段 Transfer-Encoding

當請求或響應的內容是動態的,客戶端或服務器無法預先知道要傳輸的數據大小時,就要使用 Transfer-Encoding(即 chunked 編碼傳輸)。chunked 編碼將數據分成一塊一塊的發送。chunked 編碼將使用若干個chunk 串連而成,由一個標明長度為 0 的 chunk 標示結束。每個 chunk 分為頭部和正文兩部分,頭部內容指定正文的字符總數(十六進制的數字)和數量單位(一般不寫),正文部分就是指定長度的實際內容,兩部分之間用回車換行(CRLF)隔開。在最后一個長度為 0 的 chunk 中的內容是稱為footer的內容,是一些附加的Header信息(通常可以直接忽略)。

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

好了,本章較長,雖然不是很深奧難懂的知識,也不是很牛逼的技術實現,但是耐心看完之后相信你終究是有所收獲的。在此本文就要完結了,后續會對 netty HTTP 協議棧做更深入的研究,至于這個 github 上的項目,后面也會繼續完善 TODO LIST。大家可以通過多種方式與我交流,并歡迎大家提出寶貴意見。

責任編輯:武曉燕 來源: 博客園
相關推薦

2010-06-11 14:15:23

WAP協議棧

2010-09-08 17:40:56

協議棧是什么

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編程模型

2014-11-13 10:57:03

http協議

2013-07-09 14:36:24

2020-05-22 09:12:46

HTTP3網絡協議

2020-07-09 08:14:43

TCPIP協議棧

2010-08-02 16:43:46

ICMP協議

2019-04-23 10:48:55

HTTPTomcat服務器

2010-05-25 13:20:46

http與svn

2018-04-20 09:36:23

NettyWebSocket京東

2009-06-03 15:52:34

堆內存棧內存Java內存分配

2009-08-11 14:51:11

C#數據結構與算法

2017-05-26 10:35:13

前端HTTP

2021-05-18 10:32:40

Windows操作系統漏洞

2021-05-07 09:17:21

HTTPTCP協議

2013-05-08 12:42:39

HTTP協議IIS原理ASP.NET
點贊
收藏

51CTO技術棧公眾號

青娱乐国产盛宴| 欧美性猛交xxx乱久交| 亚洲第一视频在线| 国产日韩欧美一区在线| 亚洲码在线观看| 亚洲三级视频网站| 午夜影院免费在线| 波多野洁衣一区| 日本成人在线视频网址| www日韩在线| 欧美aaaaa级| 欧美手机在线视频| 日韩xxxx视频| 国产视频二区在线观看| 久久99久久久久久久久久久| 不卡av电影在线观看| 亚洲啪av永久无码精品放毛片| 日本在线啊啊| 国产精品亲子伦对白| 亚洲最大成人免费视频| 成人精品在线看| 久久婷婷蜜乳一本欲蜜臀| 日韩午夜电影av| 熟女性饥渴一区二区三区| 成人在线免费观看| 国产激情91久久精品导航| 欧美性受xxx| 人妻少妇精品一区二区三区| 欧美男男freegayvideosroom| 欧美色图12p| 国产一线二线三线女| 成人免费在线电影| 不卡视频在线看| 国产精品一区二区性色av| 国产无码精品一区二区| 日韩在线欧美| 亚洲国产毛片完整版| 一区二区三区网址| 国模精品视频| 中文字幕亚洲综合久久菠萝蜜| 国内一区二区在线视频观看| 亚洲无码精品在线播放| 日韩视频在线一区二区三区 | 国产精品99久久久久久似苏梦涵| 久久久久久尹人网香蕉| 黄色aaa视频| 日韩精品视频中文字幕| 色狠狠色狠狠综合| 激情五月婷婷六月| 草草影院在线观看| 白白色 亚洲乱淫| 国产欧美婷婷中文| 亚洲精品在线观看av| 91日韩欧美| 国产一区二区三区久久精品| 成人免费无码大片a毛片| 在线精品视频一区| 欧美蜜桃一区二区三区| 能看的毛片网站| 中文字幕 在线观看| 亚洲1区2区3区视频| 黄色污污在线观看| 成人看片免费| 中文字幕一区视频| 伊人av成人| aaa日本高清在线播放免费观看| 久久夜色精品一区| 明星裸体视频一区二区| 天天av天天翘| 99精品国产91久久久久久| 51成人做爰www免费看网站| 国产乱淫a∨片免费观看| 蜜桃一区二区三区在线观看| 国产精品v日韩精品| 亚洲欧美另类在线视频| 欧美亚洲三区| 国产精品久久久久久久久| 国产真人无遮挡作爱免费视频| 欧美暴力喷水在线| 欧美成人精品不卡视频在线观看| 视频这里只有精品| 欧美福利专区| 欧美精品videossex性护士| 久久一二三四区| 亚洲国产日本| 欧美性一区二区三区| 青青草av在线播放| 日韩精品乱码av一区二区| 啪一啪鲁一鲁2019在线视频| 国产精品自拍99| 精品电影一区| 国产精品69久久| 国产精品久久久久久在线| 国产精品1024| 久久久com| 亚乱亚乱亚洲乱妇| 亚洲精品视频在线| 欧美 日韩 激情| 日韩av首页| 欧美一级免费大片| 看全色黄大色黄女片18| 国产探花一区二区| 精品国产自在精品国产浪潮| 久久久夜色精品| 国产美女精品| 国产在线拍揄自揄视频不卡99| 99热这里只有精品66| 成人午夜免费电影| 日韩精品一区二区三区外面 | 欧美韩国理论所午夜片917电影| 少妇影院在线观看| 最新成人av网站| 国产精品欧美一区二区三区奶水| 国产精品天天操| 99麻豆久久久国产精品免费| 亚洲激情一区二区三区| 女人天堂av在线播放| 91国偷自产一区二区开放时间| 亚洲男人天堂av在线| 国产精品sss在线观看av| 亚洲系列中文字幕| 免费中文字幕在线观看| 日韩极品在线观看| 国产精品久久久久久久天堂第1集 国产精品久久久久久久免费大片 国产精品久久久久久久久婷婷 | 老鸭窝一区二区久久精品| 亚洲在线免费看| 日韩欧美在线番号| 亚洲精品成人a在线观看| 成人久久久久久久久| 日韩高清一区| 国产午夜精品美女视频明星a级| 天天看片中文字幕| 老**午夜毛片一区二区三区| 懂色一区二区三区av片| 日韩在线资源| 狠狠色狠狠色综合日日小说| 欧美性猛交xx| 欧美偷拍综合| 91a在线视频| 亚洲爆乳无码一区二区三区| 欧美经典一区二区| 国产资源在线视频| 动漫视频在线一区| 久久综合久久八八| 中文永久免费观看| 99久精品国产| 亚洲乱码日产精品bd在线观看| 久久久免费人体| 亚洲精品国产免费| 国产精品6666| 国产白丝网站精品污在线入口| 中文字幕一区二区三区最新| 国产超碰精品| 精品视频久久久久久久| 国产午夜精品一区二区理论影院 | 久久免费视频网| 99在线精品视频免费观看20| 国产精品伦理一区二区| 欧美精品aaaa| 视频精品在线观看| 538国产精品视频一区二区| 午夜精品小视频| 国产精品灌醉下药二区| 亚洲 欧美 日韩系列| 精品欧美久久| 国产精品扒开腿做爽爽爽的视频| 黄色在线免费观看大全| 黄色成人av在线| 国产黄色三级网站| 国产一区成人| 久久久久久国产精品mv| 涩涩涩在线视频| 亚洲精品一区中文字幕乱码| 成年人av网站| 亚洲国产精品成人综合色在线婷婷 | 婷婷激情久久| 97超级碰碰碰| 天天插天天干天天操| 亚洲v中文字幕| 亚洲天堂2024| 亚洲一区二区三区高清不卡| 久久99精品久久久久久水蜜桃| 日韩电影免费看| 亚洲少妇激情视频| 久久精品偷拍视频| 国产精品国产三级国产三级人妇 | 色噜噜狠狠色综合中国| av网站免费在线看| 久久国产尿小便嘘嘘| 中文字幕在线观看一区二区三区| www欧美在线观看| 久久精品国产精品| 国产1区在线观看| 天天操天天综合网| 青青青视频在线播放| 韩国三级在线一区| 无码人妻少妇伦在线电影| 日韩激情综合| 欧美专区中文字幕| 伊人免费在线| 亚洲的天堂在线中文字幕| 欧美性猛交xxxx乱大交hd| 最新热久久免费视频| 污污内射在线观看一区二区少妇 | 国产精品88久久久久久妇女 | 天堂美国久久| 国产伦精品一区二区三区| 天然素人一区二区视频| 久久天堂电影网| 亚洲欧美自偷自拍| 欧美日韩国产片| 99精品视频99| 国产精品美女久久久久久2018| 亚洲一区和二区| 蜜臀精品久久久久久蜜臀| 99亚洲精品视频| 婷婷成人综合| 国产超碰91| 成人综合网站| 2019中文字幕免费视频| 免费超碰在线| 亚洲欧洲午夜一线一品| 欧美亚洲国产怡红院影院| 无码人妻精品一区二区三区66| 91久久久精品国产| 欧美另类高清视频在线| 在线观看亚洲精品福利片| 91a在线视频| 中文字幕在线观看播放| 国产一区二区三区在线观看网站| 国产一区二区在线播放视频| 欧美视频裸体精品| 欧洲美女女同性互添| 91美女福利视频| 日本50路肥熟bbw| 极品少妇xxxx精品少妇偷拍| 国产成人av影视| 亚洲福利电影| 在线国产99| 国产一区二区在线| 久久精品国产美女| 亚洲日本va| 国产精品视频内| 免费v片在线观看| 欧美激情欧美激情在线五月| 黄视频网站在线| 一本一本久久a久久精品综合小说| 天堂网在线观看视频| 日韩欧美中文一区二区| 国产精品久久免费| 欧美日韩一区二区电影| 亚洲无码精品一区二区三区| 欧美视频一二三| 久久久久久不卡| 欧美性生交xxxxxdddd| 欧美三级韩国三级日本三斤在线观看| 一个色在线综合| 久久亚洲成人av| 亚洲国产成人av| 天堂资源在线播放| 亚洲一区二区视频| 国产激情无码一区二区三区| 亚洲三级在线播放| 日韩一区二区不卡视频| 国产精品久久久久aaaa樱花 | 久久电影天堂| 国产精品偷伦视频免费观看国产| 欧美国产大片| 国产精品色婷婷视频| 看片一区二区| 91中文在线视频| 亚洲狼人综合| 午夜精品久久久久久久男人的天堂 | 国产亚洲一卡2卡3卡4卡新区 | 激情伊人五月天| 香蕉久久夜色精品国产| 日本成人在线免费视频| 免费在线视频一区| 国产欧美精品一二三| 国产精品亚洲а∨天堂免在线| 激情小说欧美色图| 成人福利视频网站| 法国伦理少妇愉情| 久久影院视频免费| 99久久99久久精品免费| 中文字幕一区在线| 国产一卡二卡在线| 亚洲成人激情综合网| 丁香六月婷婷综合| 在线观看成人小视频| 99精品免费观看| 91精品久久久久久久久99蜜臂| 国产高清在线免费| 亚洲第一天堂av| 懂色一区二区三区| 久久久99免费视频| 青青在线视频| 欧美性做爰毛片| 日本精品在线中文字幕| 国产色婷婷国产综合在线理论片a| 欧美一级大片在线视频| 久久精品欧美| 亚洲精品进入| 可以免费看的黄色网址| 国产一区导航| wwwwxxxx日韩| 成人av免费观看| 日本黄色激情视频| 午夜精品久久久久久久99水蜜桃 | 亚洲激情图片qvod| 国产无遮挡免费视频| 日本久久电影网| 一级特黄录像免费看| 日韩丝袜美女视频| 国产成人三级在线观看视频| 最近2019中文字幕在线高清| 国产啊啊啊视频在线观看| 欧美精品成人91久久久久久久| 在线观看网站免费入口在线观看国内| 国产日韩视频在线观看| 午夜电影一区| 免费影院在线观看一区| 香蕉视频官网在线观看日本一区二区| 无码aⅴ精品一区二区三区浪潮| 国产在线国偷精品产拍免费yy| 美女洗澡无遮挡| 亚洲aaa精品| h狠狠躁死你h高h| 亚洲一二三在线| 国产福利电影在线播放| 91日本在线视频| 亚洲婷婷影院| 国产精品无码电影在线观看 | www.com操| 久久久噜噜噜久久人人看 | 91九色精品| 爱情岛论坛vip永久入口| 97aⅴ精品视频一二三区| 校园春色 亚洲| 欧美日韩国产123区| 亚洲区小说区图片区| 欧美日韩电影在线观看| 国产精品麻豆成人av电影艾秋| 欧美在线视频二区| 国产亚洲精品bv在线观看| 久久国产劲爆∧v内射| 依依成人精品视频| 国产美女无遮挡永久免费| 色777狠狠综合秋免鲁丝| 亚洲成人激情社区| 欧美在线视频一区二区三区| 麻豆成人精品| 中文字幕国产专区| 色婷婷综合久久久久中文 | 国产成人在线视频免费播放| 精品国产大片大片大片| 91国产丝袜在线播放| 免费黄网站在线观看| 国产成人在线一区| 狠狠色狠狠色综合婷婷tag| 精品欧美一区免费观看α√| 成人av午夜电影| 久草视频在线观| 精品亚洲一区二区三区四区五区| 中文av在线全新| 欧洲视频一区二区三区| 久久综合伊人| 亚欧精品视频一区二区三区| 富二代精品短视频| av片免费播放| 久久97精品久久久久久久不卡| 国产精品亚洲综合在线观看| 在线国产99| 蜜臀91精品一区二区三区| 男人的天堂官网| 欧美日韩在线亚洲一区蜜芽| av在线天堂播放| 国产欧美一区二区三区久久| 国产精品成久久久久| 日韩精品aaa| 亚洲午夜精品在线| 亚洲av无码乱码国产麻豆| 久久久久久这里只有精品| 在线精品国产亚洲| 久久久久久久香蕉| 91免费看`日韩一区二区| 亚洲中文一区二区| 久久视频这里只有精品| 高潮按摩久久久久久av免费| bt天堂新版中文在线地址| 91麻豆精品视频| 亚洲中文字幕一区二区| 久久久精品在线观看| 成人看片爽爽爽| 久久精品免费网站| 亚洲码国产岛国毛片在线| 亚洲av成人精品日韩在线播放| 国产精品一二三在线| 狠狠干综合网| 天天操天天舔天天射|