Netty實現Http高性能服務器


淺談HTTP Method

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

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

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

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

什麼是HTTP方法?

有寫過網頁表單的人必定對GET與POST不陌生,但你瞭解什麼是GETPOST嗎!?現今的網頁設計工具至關的發達,甚至不須要接觸HTML語法就能完成一個規模不小的網站,漸漸地不少人都忘記了HTTP底層的實做原理,形成在發生錯誤的狀況下沒法正確進行偵錯。web

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

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

  • GETbash

  • HEAD

  • POST

  • PUT

  • DELETE

  • TRACE

  • CONNECT

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

GET與POST方法

先舉個例子,若是HTTP 表明如今咱們現實生活中寄信的機制。

:speaker:那麼信封的撰寫格式就是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 以後瀏覽器的網址就變成"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 進行傳送,在不偷看封包的狀況下彷佛安全一些些.......:yellow_heart: 。此外在傳送檔案的時候會使用到multi-part 編碼,將檔案與其餘的表單欄位一併放在message-body 中進行傳送。這就是GET 與POST 發送表單的差別囉。

Netty HTTP編解碼

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

NettyHTTP編解碼器

public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        /**
         * 或者使用HttpRequestDecoder & HttpResponseEncoder
         */
        p.addLast(new HttpServerCodec());
        /**
         * 在處理POST消息體時須要加上
         */
        p.addLast(new HttpObjectAggregator(1024*1024));
        p.addLast(new HttpServerExpectContinueHandler());
        p.addLast(new HttpHelloWorldServerHandler());
    }
}
複製代碼
  • 第 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)){
&emsp;&emsp;QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
&emsp;&emsp;Map<String, List<String>> uriAttributes = queryDecoder.parameters();
&emsp;&emsp;//此處僅打印請求參數(你能夠根據業務需求自定義處理)
&emsp;&emsp;for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
&emsp;&emsp;&emsp;&emsp;for (String attrVal : attr.getValue()) {
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;System.out.println(attr.getKey() + "=" + attrVal);
&emsp;&emsp;&emsp;&emsp;}
&emsp;&emsp;}
}
複製代碼

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服務器 項目工程地址: github.com/sanshengshu…

原創不易,若是感受不錯,但願給個推薦!您的支持是我寫做的最大動力!

版權聲明:

做者:穆書偉

博客園出處:www.cnblogs.com/sanshengshu…

github出處:github.com/sanshengshu…    

我的博客出處:sanshengshui.github.io/

相關文章
相關標籤/搜索