Netty實現高性能的HTTP服務器



 

 

 

淺談HTTP Method

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

HTTP在客戶端 - 服務器計算模型中用作請求 - 響應協議。

例如,web瀏覽器可以是客戶端,並且在托管網站的計算機上運行的應用程序可以是服務器。 客戶端向服務器提交HTTP請求消息。

服務器提供諸如HTML文件和其他內容之類的資源,或代表客戶端執行其他功能,向客戶端返回響應消息。 響應包含有關請求的完成狀態信息,並且還可以在其消息正文中包含所請求的內容。

 

什么是HTTP方法?

有寫過網頁表單的人一定對GET與POST不陌生,但你了解什么是GETPOST嗎!?現今的網頁設計工具相當的發達,甚至不需要接觸HTML語法就能完成一個規模不小的網站,漸漸地很多人都忘記了HTTP底層的實作原理,造成在發生錯誤的情況下無法正確進行偵錯。

早期在撰寫HTML 表單語法時,都會寫到以下的寫法,然而大部分的軟件工程師都會采用POST 進行表單傳送。

 <form action="" method="POST/GET">
  </form>

然而在我們的網頁程序中要獲取表單的變數只需要調用系統已經封裝好的方法即可,像是PHP使用$_REQUEST、JAVA使用getParameter()、ASP使用Request.Form()這些方法等等。 由上述的方法看來,似乎用POST或GET好像不是很重要。許多Web工程師對於表單method用法的記憶為"POST可以傳送比較多的資料"、"表單傳送檔案的時候要使用POST"、"POST比GET安全"等等奇怪的概念。

其實使用POST 或GET 其實是有差別的,我們先說明一下HTTP Method,在HTTP 1.1 的版本中定義了八種Method (方法),如下所示:

  • OPTIONS

  • GET

  • HEAD

  • POST

  • PUT

  • DELETE

  • TRACE

  • CONNECT

天阿!這些方法看起來真是陌生。而我們使用的表單只用了其中兩個方法(GET/POST),其他的方法確實很少用到,但是在RESTful 的設計架構中就會使用到更多的Method 來簡化設計。

 

GET與POST方法

先舉個例子,如果HTTP 代表現在我們現實生活中寄信的機制。

🔈那么信封的撰寫格式就是HTTP。我們姑且將信封外的內容稱為http-header,信封內的書信稱為message-body,那么HTTP Method 就是你要告訴郵差的寄信規則。

假設GET 表示信封內不得裝信件的寄送方式,如同是明信片一樣,你可以把要傳遞的資訊寫在信封(http-header)上,寫滿為止,價格比較便宜。然而POST 就是信封內有裝信件的寄送方式(信封有內容物),不但信封可以寫東西,信封內(message-body) 還可以置入你想要寄送的資料或檔案,價格較貴。

使用GET 的時候我們直接將要傳送的資料以Query String(一種Key/Vaule的編碼方式)加在我們要寄送的地址(URL)后面,然后交給郵差傳送。

使用POST 的時候則是將寄送地址(URL)寫在信封上,另外將要傳送的資料寫在另一張信紙后,將信紙放到信封里面,交給郵差傳送。

GET方法

接着我來介紹一下實際的運作情況:

我們先來看看GET 怎么傳送資料的,當我們送出一個GET 表單時,如下范例:

  <form method="get" action="">
  <input type="text" name="id" />
  <input type="submit" />
  </form>

當表單Submit 之后瀏覽器的網址就變成"http://xxx.toright.com/?id=010101",瀏覽器會自動將表單內容轉為Query String 加在URL 進行連線。

這時后來看一下HTTP Request 封包的內容:

GET /?id=010101 HTTP/1.1
  Host: xxx.toright.com
  User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 GTB7.1 ( .NET CLR 3.5.30729)
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
  Accept-Encoding: gzip,deflate
  Accept-Charset: UTF-8,*
  Keep-Alive: 115
  Connection: keep-alive

 

在HTTP GET Method 中是不允許在message-body 中傳遞資料的,因為是GET 嘛,就是要取資料的意思。

從瀏覽器的網址列就可以看見我們表單要傳送的資料,若是要傳送密碼豈不是"一覽無遺".......這就是大家常提到安全性問題。

POST方法

再來看看POST 傳送資料

 <form method="post" action="">
  <input type="text" name="id" />
  <input type="submit" />
  </form>

 

網址列沒有變化,那我們來看一下HTTP Request 封包的內容:

 POST / HTTP/1.1
  Host: xxx.toright.com
  User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 GTB7.1 ( .NET CLR 3.5.30729)
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
  Accept-Encoding: gzip,deflate
  Accept-Charset: UTF-8,*
  Keep-Alive: 115
  Connection: keep-alive
   
  Content-Type: application/x-www-form-urlencoded
  </code><code>Content-Length: 9
  id=020202

看出個所以然了嗎?原來POST 是將表單資料放在message-body 進行傳送,在不偷看封包的情況下似乎安全一些些.......💛 。此外在傳送檔案的時候會使用到multi-part 編碼,將檔案與其他的表單欄位一並放在message-body 中進行傳送。這就是GET 與POST 發送表單的差異啰。

 

 

Netty HTTP編解碼

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

 

NettyHTTP編解碼器

 1   public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {
 2       @Override
 3       public void initChannel(SocketChannel ch) {
 4           ChannelPipeline p = ch.pipeline();
 5           /**
 6            * 或者使用HttpRequestDecoder & HttpResponseEncoder
 7            */
 8           p.addLast(new HttpServerCodec());
 9           /**
10            * 在處理POST消息體時需要加上
11            */
12           p.addLast(new HttpObjectAggregator(1024*1024));
13           p.addLast(new HttpServerExpectContinueHandler());
14           p.addLast(new HttpHelloWorldServerHandler());
15       }
16   }

 

  • 第 8 行:調用#new HttpServerCodec()方法,編解碼器支持部分 HTTP 請求解析,比如 HTTP GET請求所傳遞的參數是包含在 uri 中的,因此通過 HttpRequest 既能解析出請求參數。

    • HttpRequestDecoder 即把 ByteBuf 解碼到 HttpRequest 和 HttpContent。

    • HttpResponseEncoder 即把 HttpResponse 或 HttpContent 編碼到 ByteBuf。

    • HttpServerCodec 即 HttpRequestDecoder 和 HttpResponseEncoder 的結合。

 

但是,對於 HTTP POST 請求,參數信息是放在 message body 中的(對應於 netty 來說就是 HttpMessage),所以以上編解碼器並不能完全解析 HTTP POST請求。

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

  • 第 12 行:調用#new HttpObjectAggregator(1024*1024)方法,即通過它可以把 HttpMessage 和 HttpContent 聚合成一個 FullHttpRequest 或者 FullHttpResponse (取決於是處理請求還是響應),而且它還可以幫助你在解碼時忽略是否為“塊”傳輸方式。

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

  • 第13行: 這個方法的作用是: http 100-continue用於客戶端在發送POST數據給服務器前,征詢服務器情況,看服務器是否處理POST的數據,如果不處理,客戶端則不上傳POST數據,如果處理,則POST上傳數據。在現實應用中,通過在POST大數據時,才會使用100-continue協議

 

HTTP 響應消息的實現

我們把 Java 對象根據HTTP協議封裝成二進制數據包的過程成為編碼,而把從二進制數據包中解析出 Java 對象的過程成為解碼,在學習如何使用 Netty 進行HTTP協議的編解碼之前,我們先來定義一下客戶端與服務端通信的 Java 對象。

 

Java 對象

我們如下定義通信過程中的 Java 對象

  @Data
  public class User {
      private String userName;
  ​
      private String method;
  ​
      private Date date;
  }

 

  1. 以上是通信過程中 Java 對象的抽象類,可以看到,我們定義了一個用戶名(默認值為 sanshengshui )以及一個http請求的方法和當前時間日期。

  2. @Data 注解由 lombok 提供,它會自動幫我們生產 getter/setter 方法,減少大量重復代碼,推薦使用

Java 對象定義完成之后,接下來我們就需要定義一種規則,如何把一個 Java 對象轉換成二進制數據,這個規則叫做 Java 對象的序列化。

 

序列化

我們如下定義序列化接口

  /**
   * 序列化接口類
   */
  public interface Serializer {
      /**
       * java 對象轉換成二進制
       */
      byte[] serialize(Object object);
  ​
      /**
       * 二進制轉換成 java 對象
       */
      <T> T deserialize(Class<T> clazz, byte[] bytes);
  }

 

序列化接口有二個方法,serialize() 將 Java 對象轉換成字節數組,deserialize() 將字節數組轉換成某種類型的 Java 對象,在工程中,我們使用最簡單的 json 序列化方式,使用阿里巴巴的 fastjson 作為序列化框架。

  public class JSONSerializer implements Serializer {
      @Override
      public byte[] serialize(Object object) {
          return JSON.toJSONBytes(object);
      }
  ​
      @Override
      public <T> T deserialize(Class<T> clazz, byte[] bytes) {
          return JSON.parseObject(bytes,clazz);
      }
  }

 

編碼

  User user = new User();
          user.setUserName("sanshengshui");
          user.setDate(new Date());
          user.setMethod("get");
          JSONSerializer jsonSerializer = new JSONSerializer();
          //將Java對象序列化成為二級制數據包
          byte[] content = jsonSerializer.serialize(user);
          FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(content));
          response.headers().set(CONTENT_TYPE, "text/plain");
          response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
  ​
          boolean keepAlive = HttpUtil.isKeepAlive(request);
          if (!keepAlive) {
              ctx.write(response).addListener(ChannelFutureListener.CLOSE);
             } else {
              response.headers().set(CONNECTION, KEEP_ALIVE);
              ctx.write(response);
             }

 

 

HTTP GET解析實踐

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

  HttpRequest request = (HttpRequest) msg;
  String uri = request.uri();

 

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

  if(uri.equals(FAVICON_ICO)){
      return;
  }

接下來就是解析 uri 了。這里需要用到 QueryStringDecoder

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

 

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

解析代碼如下:

  String uri = request.uri();
  HttpMethod method = request.method();
  if(method.equals(HttpMethod.GET)){
    QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
    Map<String, List<String>> uriAttributes = queryDecoder.parameters();
    //此處僅打印請求參數(你可以根據業務需求自定義處理)
    for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
      for (String attrVal : attr.getValue()) {
        System.out.println(attr.getKey() + "=" + attrVal);
      }
    }
  }

 

 

HTTP POST 解析實踐

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

首先解釋下 FullHttpRequest 是什么:

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

 

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

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

  FullHttpRequest fullRequest = (FullHttpRequest) msg;

 

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

  private void dealWithContentType() throws Exception{
          String contentType = getContentType();
          //可以使用HttpJsonDecoder
          if(contentType.equals("application/json")){
              String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
              JSONObject obj = JSON.parseObject(jsonStr);
              for(Map.Entry<String, Object> item : obj.entrySet()){
                  logger.info(item.getKey()+"="+item.getValue().toString());
              }
  ​
          }else if(contentType.equals("application/x-www-form-urlencoded")){
              //方式一:使用 QueryStringDecoder
              String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
              QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false);
              Map<String, List<String>> uriAttributes = queryDecoder.parameters();
              for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
                  for (String attrVal : attr.getValue()) {
                      logger.info(attr.getKey() + "=" + attrVal);
                  }
              }
  ​
          }else if(contentType.equals("multipart/form-data")){
              //TODO 用於文件上傳
          }else{
              //do nothing...
          }
      }
      private String getContentType(){
          String typeStr = headers.get("Content-Type").toString();
          String[] list = typeStr.split(";");
          return list[0];
      }

 

 

功能測試

我是利用Postman對netty實現的http服務器進行請求,大家如果覺的可以的話,可以自行下載。

Get 請求

Postman:

 

Server:

  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
  //打印請求url
  16:58:59.159 [nioEventLoopGroup-3-1] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: 

 

 

Post 請求

Postman:

 

Server:

  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
  16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
  16:58:59.159 [nioEventLoopGroup-3-1] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: /
  17:03:59.813 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x0f3f5fdd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xfd00cb1b, L:/0:0:0:0:0:0:0:1:8888 - R:/0:0:0:0:0:0:0:1:45768]
  17:03:59.813 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x0f3f5fdd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
  //打印post請求的url
  17:03:59.817 [nioEventLoopGroup-3-2] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: /ttt

 

 

Gatling性能,負載測試

如果對Gatling測試工具不太熟悉的話,可以看一下我之前寫的文章:

  1. 負載,性能測試工具-Gatling

  2. Gatling簡單測試SpringBoot工程

性能測試報告大體如下:

================================================================================
  ---- Global Information --------------------------------------------------------
  > request count                                    1178179 (OK=1178179 KO=0     )
  > min response time                                      0 (OK=0      KO=-     )
  > max response time                                  12547 (OK=12547  KO=-     )
  > mean response time                                     1 (OK=1      KO=-     )
  > std deviation                                         32 (OK=32     KO=-     )
  > response time 50th percentile                          0 (OK=0      KO=-     )
  > response time 75th percentile                          1 (OK=1      KO=-     )
  > response time 95th percentile                          2 (OK=2      KO=-     )
  > response time 99th percentile                          5 (OK=5      KO=-     )
  > mean requests/sec                                10808.982 (OK=10808.982 KO=-     )
  ---- Response Time Distribution ------------------------------------------------
  > t < 800 ms                                       1178139 (100%)
  > 800 ms < t < 1200 ms                                   0 (  0%)
  > t > 1200 ms                                           40 (  0%)
  > failed                                                 0 (  0%)
  ================================================================================

 

 

 

其他

關於Netty實現高性能的HTTP服務器詳解到這里就結束了。

Netty實現高性能的HTTP服務器 項目工程地址: https://github.com/sanshengshui/netty-learning-example/tree/master/netty-http

原創不易,如果感覺不錯,希望給個推薦!您的支持是我寫作的最大動力!

版權聲明:

作者:穆書偉

博客園出處:https://www.cnblogs.com/sanshengshui

github出處:https://github.com/sanshengshui    

個人博客出處:https://sanshengshui.github.io/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM