Netty實現高效能的HTTP伺服器
淺談HTTP Method
要通過netty實現HTTP伺服器(或者客戶端),首先你要了解HTTP協議。
HTTP在客戶端 - 伺服器計算模型中用作 請求 - 響應 協議。
例如,web瀏覽器可以是客戶端,並且在託管網站的計算機上執行的應用程式可以是伺服器。 客戶端向伺服器提交HTTP請求訊息。
伺服器提供諸如HTML檔案和其他內容之類的資源,或代表客戶端執行其他功能,向客戶端返回響應訊息。 響應包含有關請求的完成狀態資訊,並且還可以在其訊息正文中包含所請求的內容。
什麼是HTTP方法?
有寫過網頁表單的人一定對GET與POST不陌生,但你瞭解什麼是 GET 與 POST 嗎!?現今的網頁設計工具相當的發達,甚至不需要接觸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 代表現在我們現實生活中寄信的機制。
: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 之後瀏覽器的網址就變成" ofollow,noindex" target="_blank">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 進行傳送,在不偷看封包的情況下似乎安全一些些....... :yellow_heart: 。此外在傳送檔案的時候會使用到multi-part 編碼,將檔案與其他的表單欄位一併放在message-body 中進行傳送。這就是GET 與POST 傳送表單的差異囉。
Netty HTTP編解碼
要通過 Netty 處理 HTTP 請求,需要先進行編解碼。
NettyHTTP編解碼器
1public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> { 2@Override 3public void initChannel(SocketChannel ch) { 4ChannelPipeline p = ch.pipeline(); 5/** 6* 或者使用HttpRequestDecoder & HttpResponseEncoder 7*/ 8p.addLast(new HttpServerCodec()); 9/** 10* 在處理POST訊息體時需要加上 11*/ 12p.addLast(new HttpObjectAggregator(1024*1024)); 13p.addLast(new HttpServerExpectContinueHandler()); 14p.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; }
-
以上是通訊過程中 Java 物件的抽象類,可以看到,我們定義了一個使用者名稱(預設值為 sanshengshui )以及一個http請求的方法和當前時間日期。
-
@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測試工具不太熟悉的話,可以看一下我之前寫的文章:
效能測試報告大體如下:
================================================================================ ---- Global Information -------------------------------------------------------- > request count1178179 (OK=1178179 KO=0) > min response time0 (OK=0KO=-) > max response time12547 (OK=12547KO=-) > mean response time1 (OK=1KO=-) > std deviation32 (OK=32KO=-) > response time 50th percentile0 (OK=0KO=-) > response time 75th percentile1 (OK=1KO=-) > response time 95th percentile2 (OK=2KO=-) > response time 99th percentile5 (OK=5KO=-) > mean requests/sec10808.982 (OK=10808.982 KO=-) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms1178139 (100%) > 800 ms < t < 1200 ms0 (0%) > t > 1200 ms40 (0%) > failed0 (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/