一、HTTP協議簡介
應用層協議http,發展至今已經是http2.0了,擁有以下特點:
(1) CS模式的協議
(2) 簡單 - 只需要服務URL,攜帶必要的請求參數或者消息體
(3) 靈活 - 任意類型,傳輸內容類型由HTTP消息頭中的Content-Type加以標記
(4) 無狀態 - 必須借助額外手段,比如session或者cookie來保持狀態
1.1 HTTP請求消息(HttpRequest)
客戶端發送一個HTTP請求到服務器的請求消息包括以下格式:請求行(request line)、請求頭部(header)、空行和請求數據四個部分組成,下圖給出了請求報文的一般格式。
舉個例子:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
請求方法:
根據HTTP標准,HTTP請求可以使用多種請求方法。
HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
HTTP1.1新增了五種請求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
序號 | 方法 | 描述 |
---|---|---|
1 | GET | 請求指定的頁面信息,並返回實體主體。 |
2 | HEAD | 類似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭 |
3 | POST | 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件)。數據被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。 |
4 | PUT | 從客戶端向服務器傳送的數據取代指定的文檔的內容。 |
5 | DELETE | 請求服務器刪除指定的頁面。 |
6 | CONNECT | HTTP/1.1協議中預留給能夠將連接改為管道方式的代理服務器。 |
7 | OPTIONS | 允許客戶端查看服務器的性能。 |
8 | TRACE | 回顯服務器收到的請求,主要用於測試或診斷。 |
GET方法:參數在請求行,不安全且有一定限制
POST方法:要求在服務器接受后面的數據,常用於提交表單。
一般GET用於獲取/查詢信息,而POST一般用於創建,更新信息。二者主要區別如下:
(1) 根據HTTP規范,GET用於獲取,應該是安全和冪等的,而POST則表示可能改變服務器上的資源;
(2) GET請求數據會附在URL上,即請求行中,以"?"分隔URL和傳輸數據,多個參數用&連接;而POST會把數據放在HTTP消息的報體中,地址欄中沒有
(3) 傳輸數據的大小不同,特定瀏覽器有限制,例如IE對URL限制是2083字節,POST理論上沒有限制
(4) POST更安全,使用GET還有可能受到Cross-site request forgery攻擊等等。
部分請求頭部說明:
Header | 解釋 | 示例 |
---|---|---|
Accept | 指定客戶端能夠接收的內容類型 | Accept: text/plain, text/html |
Accept-Charset | 瀏覽器可以接受的字符編碼集。 | Accept-Charset: iso-8859-5 |
Accept-Encoding | 指定瀏覽器可以支持的web服務器返回內容壓縮編碼類型。 | Accept-Encoding: compress, gzip |
Accept-Language | 瀏覽器可接受的語言 | Accept-Language: en,zh |
Accept-Ranges | 可以請求網頁實體的一個或者多個子范圍字段 | Accept-Ranges: bytes |
Authorization | HTTP授權的授權證書 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Cache-Control | 指定請求和響應遵循的緩存機制 | Cache-Control: no-cache |
Connection | 表示是否需要持久連接。(HTTP 1.1默認進行持久連接) | Connection: close |
Cookie | HTTP請求發送時,會把保存在該請求域名下的所有cookie值一起發送給web服務器。 | Cookie: $Version=1; Skin=new; |
Content-Length | 請求的內容長度 | Content-Length: 348 |
Content-Type | 請求的與實體對應的MIME信息 | Content-Type: application/x-www-form-urlencoded |
Date | 請求發送的日期和時間 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
Expect | 請求的特定的服務器行為 | Expect: 100-continue |
From | 發出請求的用戶的Email | From: user@email.com |
Host | 指定請求的服務器的域名和端口號 | Host: www.zcmhi.com |
If-Match | 只有請求內容與實體相匹配才有效 | If-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Modified-Since | 如果請求的部分在指定時間之后被修改則請求成功,未被修改則返回304代碼 | If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
If-None-Match | 如果內容未改變返回304代碼,參數為服務器先前發送的Etag,與服務器回應的Etag比較判斷是否改變 | If-None-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Range | 如果實體未改變,服務器發送客戶端丟失的部分,否則發送整個實體。參數也為Etag | If-Range: “737060cd8c284d8af7ad3082f209582d” |
If-Unmodified-Since | 只在實體在指定時間之后未被修改才請求成功 | If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
Max-Forwards | 限制信息通過代理和網關傳送的時間 | Max-Forwards: 10 |
Pragma | 用來包含實現特定的指令 | Pragma: no-cache |
Proxy-Authorization | 連接到代理的授權證書 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Range | 只請求實體的一部分,指定范圍 | Range: bytes=500-999 |
Referer | 先前網頁的地址,當前請求網頁緊隨其后,即來路 | Referer: http://www.zcmhi.com/archives/71.html |
TE | 客戶端願意接受的傳輸編碼,並通知服務器接受接受尾加頭信息 | TE: trailers,deflate;q=0.5 |
Upgrade | 向服務器指定某種傳輸協議以便服務器進行轉換(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
User-Agent | User-Agent的內容包含發出請求的用戶信息 | User-Agent: Mozilla/5.0 (Linux; X11) |
Via | 通知中間網關或代理服務器地址,通信協議 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
Warning | 關於消息實體的警告信息 | Warn: 199 Miscellaneous warning |
1.2 HTTP響應消息
HTTP響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文。
HTTP狀態碼
當瀏覽者訪問一個網頁時,瀏覽者的瀏覽器會向網頁所在服務器發出請求。當瀏覽器接收並顯示網頁前,此網頁所在的服務器會返回一個包含HTTP狀態碼的信息頭(server header)用以響應瀏覽器的請求。
HTTP狀態碼的英文為HTTP Status Code。
下面是常見的HTTP狀態碼:
- 200 - 請求成功
- 301 - 資源(網頁等)被永久轉移到其它URL
- 404 - 請求的資源(網頁等)不存在
- 500 - 內部服務器錯誤
HTTP狀態碼分類
HTTP狀態碼由三個十進制數字組成,第一個十進制數字定義了狀態碼的類型,后兩個數字沒有分類的作用。HTTP狀態碼共分為5種類型:
分類 | 分類描述 |
---|---|
1** | 信息,服務器收到請求,需要請求者繼續執行操作 |
2** | 成功,操作被成功接收並處理 |
3** | 重定向,需要進一步的操作以完成請求 |
4** | 客戶端錯誤,請求包含語法錯誤或無法完成請求 |
5** | 服務器錯誤,服務器在處理請求的過程中發生了錯誤 |
HTTP狀態碼列表:
狀態碼 | 狀態碼英文名稱 | 中文描述 |
---|---|---|
100 | Continue | 繼續。客戶端應繼續其請求 |
101 | Switching Protocols | 切換協議。服務器根據客戶端的請求切換協議。只能切換到更高級的協議,例如,切換到HTTP的新版本協議 |
200 | OK | 請求成功。一般用於GET與POST請求 |
201 | Created | 已創建。成功請求並創建了新的資源 |
202 | Accepted | 已接受。已經接受請求,但未處理完成 |
203 | Non-Authoritative Information | 非授權信息。請求成功。但返回的meta信息不在原始的服務器,而是一個副本 |
204 | No Content | 無內容。服務器成功處理,但未返回內容。在未更新網頁的情況下,可確保瀏覽器繼續顯示當前文檔 |
205 | Reset Content | 重置內容。服務器處理成功,用戶終端(例如:瀏覽器)應重置文檔視圖。可通過此返回碼清除瀏覽器的表單域 |
206 | Partial Content | 部分內容。服務器成功處理了部分GET請求 |
300 | Multiple Choices | 多種選擇。請求的資源可包括多個位置,相應可返回一個資源特征與地址的列表用於用戶終端(例如:瀏覽器)選擇 |
301 | Moved Permanently | 永久移動。請求的資源已被永久的移動到新URI,返回信息會包括新的URI,瀏覽器會自動定向到新URI。今后任何新的請求都應使用新的URI代替 |
302 | Found | 臨時移動。與301類似。但資源只是臨時被移動。客戶端應繼續使用原有URI |
303 | See Other | 查看其它地址。與301類似。使用GET和POST請求查看 |
304 | Not Modified | 未修改。所請求的資源未修改,服務器返回此狀態碼時,不會返回任何資源。客戶端通常會緩存訪問過的資源,通過提供一個頭信息指出客戶端希望只返回在指定日期之后修改的資源 |
305 | Use Proxy | 使用代理。所請求的資源必須通過代理訪問 |
306 | Unused | 已經被廢棄的HTTP狀態碼 |
307 | Temporary Redirect | 臨時重定向。與302類似。使用GET請求重定向 |
400 | Bad Request | 客戶端請求的語法錯誤,服務器無法理解 |
401 | Unauthorized | 請求要求用戶的身份認證 |
402 | Payment Required | 保留,將來使用 |
403 | Forbidden | 服務器理解請求客戶端的請求,但是拒絕執行此請求 |
404 | Not Found | 服務器無法根據客戶端的請求找到資源(網頁)。通過此代碼,網站設計人員可設置"您所請求的資源無法找到"的個性頁面 |
405 | Method Not Allowed | 客戶端請求中的方法被禁止 |
406 | Not Acceptable | 服務器無法根據客戶端請求的內容特性完成請求 |
407 | Proxy Authentication Required | 請求要求代理的身份認證,與401類似,但請求者應當使用代理進行授權 |
408 | Request Time-out | 服務器等待客戶端發送的請求時間過長,超時 |
409 | Conflict | 服務器完成客戶端的PUT請求是可能返回此代碼,服務器處理請求時發生了沖突 |
410 | Gone | 客戶端請求的資源已經不存在。410不同於404,如果資源以前有現在被永久刪除了可使用410代碼,網站設計人員可通過301代碼指定資源的新位置 |
411 | Length Required | 服務器無法處理客戶端發送的不帶Content-Length的請求信息 |
412 | Precondition Failed | 客戶端請求信息的先決條件錯誤 |
413 | Request Entity Too Large | 由於請求的實體過大,服務器無法處理,因此拒絕請求。為防止客戶端的連續請求,服務器可能會關閉連接。如果只是服務器暫時無法處理,則會包含一個Retry-After的響應信息 |
414 | Request-URI Too Large | 請求的URI過長(URI通常為網址),服務器無法處理 |
415 | Unsupported Media Type | 服務器無法處理請求附帶的媒體格式 |
416 | Requested range not satisfiable | 客戶端請求的范圍無效 |
417 | Expectation Failed | 服務器無法滿足Expect的請求頭信息 |
500 | Internal Server Error | 服務器內部錯誤,無法完成請求 |
501 | Not Implemented | 服務器不支持請求的功能,無法完成請求 |
502 | Bad Gateway | 充當網關或代理的服務器,從遠端服務器接收到了一個無效的請求 |
503 | Service Unavailable | 由於超載或系統維護,服務器暫時的無法處理客戶端的請求。延時的長度可包含在服務器的Retry-After頭信息中 |
504 | Gateway Time-out | 充當網關或代理的服務器,未及時從遠端服務器獲取請求 |
505 | HTTP Version not supported | 服務器不支持請求 |
響應頭信息
Header | 解釋 | 示例 |
---|---|---|
Accept-Ranges | 表明服務器是否支持指定范圍請求及哪種類型的分段請求 | Accept-Ranges: bytes |
Age | 從原始服務器到代理緩存形成的估算時間(以秒計,非負) | Age: 12 |
Allow | 對某網絡資源的有效的請求行為,不允許則返回405 | Allow: GET, HEAD |
Cache-Control | 告訴所有的緩存機制是否可以緩存及哪種類型 | Cache-Control: no-cache |
Content-Encoding | web服務器支持的返回內容壓縮編碼類型。 | Content-Encoding: gzip |
Content-Language | 響應體的語言 | Content-Language: en,zh |
Content-Length | 響應體的長度 | Content-Length: 348 |
Content-Location | 請求資源可替代的備用的另一地址 | Content-Location: /index.htm |
Content-MD5 | 返回資源的MD5校驗值 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== |
Content-Range | 在整個返回體中本部分的字節位置 | Content-Range: bytes 21010-47021/47022 |
Content-Type | 返回內容的MIME類型 | Content-Type: text/html; charset=utf-8 |
Date | 原始服務器消息發出的時間 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
ETag | 請求變量的實體標簽的當前值 | ETag: “737060cd8c284d8af7ad3082f209582d” |
Expires | 響應過期的日期和時間 | Expires: Thu, 01 Dec 2010 16:00:00 GMT |
Last-Modified | 請求資源的最后修改時間 | Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT |
Location | 用來重定向接收方到非請求URL的位置來完成請求或標識新的資源 | Location: http://www.zcmhi.com/archives/94.html |
Pragma | 包括實現特定的指令,它可應用到響應鏈上的任何接收方 | Pragma: no-cache |
Proxy-Authenticate | 它指出認證方案和可應用到代理的該URL上的參數 | Proxy-Authenticate: Basic |
refresh | 應用於重定向或一個新的資源被創造,在5秒之后重定向(由網景提出,被大部分瀏覽器支持) |
Refresh: 5; url=
http://www.zcmhi.com/archives/94.html
|
Retry-After | 如果實體暫時不可取,通知客戶端在指定時間之后再次嘗試 | Retry-After: 120 |
Server | web服務器軟件名稱 | Server: Apache/1.3.27 (Unix) (Red-Hat/Linux) |
Set-Cookie | 設置Http Cookie | Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1 |
Trailer | 指出頭域在分塊傳輸編碼的尾部存在 | Trailer: Max-Forwards |
Transfer-Encoding | 文件傳輸編碼 | Transfer-Encoding:chunked |
Vary | 告訴下游代理是使用緩存響應還是從原始服務器請求 | Vary: * |
Via | 告知代理客戶端響應是通過哪里發送的 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
Warning | 警告實體可能存在的問題 | Warning: 199 Miscellaneous warning |
WWW-Authenticate | 表明客戶端請求實體應該使用的授權方案 | WWW-Authenticate: Basic |
二、 HTTP開發入門-靜態文件服務器
netty天生異步事件驅動的架構,無論是在性能上還是在可靠性上,都表現優異,非常適合在非Web容器的場景下應用,相比於傳統的Tomcat,Jetty等Web容器,更加的輕量和小巧、靈活性和定制性也更好。
我們以文件服務器為例學習Netty的HTTP服務端入門開發,例程場景如下:
- 文件服務器使用HTTP協議對外提供服務
- 當客戶端通過瀏覽器訪問文件服務器時,對訪問路徑進行檢查,檢查失敗返回403
- 檢查通過,以鏈接的方式打開當前文件目錄,每個目錄或者都是個超鏈接,可以遞歸訪問
- 如果是目錄,可以繼續遞歸訪問它下面的目錄或者文件,如果是文件並且可讀,則可以在瀏覽器端直接打開,或者通過[目標另存為]下載
1 import io.netty.bootstrap.ServerBootstrap; 2 import io.netty.channel.ChannelFuture; 3 import io.netty.channel.ChannelInitializer; 4 import io.netty.channel.EventLoopGroup; 5 import io.netty.channel.nio.NioEventLoopGroup; 6 import io.netty.channel.socket.SocketChannel; 7 import io.netty.channel.socket.nio.NioServerSocketChannel; 8 import io.netty.handler.codec.http.HttpObjectAggregator; 9 import io.netty.handler.codec.http.HttpRequestDecoder; 10 import io.netty.handler.codec.http.HttpResponseEncoder; 11 import io.netty.handler.stream.ChunkedWriteHandler; 12 13 /** 14 * @author lilinfeng 15 * @version 1.0 16 * @date 2014年2月14日 17 */ 18 public class HttpFileServer { 19 20 private static final String DEFAULT_URL = "/"; 21 22 public void run(final int port, final String url) throws Exception { 23 EventLoopGroup bossGroup = new NioEventLoopGroup(); 24 EventLoopGroup workerGroup = new NioEventLoopGroup(); 25 try { 26 ServerBootstrap b = new ServerBootstrap(); 27 b.group(bossGroup, workerGroup) 28 .channel(NioServerSocketChannel.class) 29 .childHandler(new ChannelInitializer<SocketChannel>() { 30 @Override 31 protected void initChannel(SocketChannel ch) 32 throws Exception { 33 ch.pipeline().addLast("http-decoder", 34 new HttpRequestDecoder()); // 請求消息解碼器 35 ch.pipeline().addLast("http-aggregator", 36 new HttpObjectAggregator(65536));// 目的是將多個消息轉換為單一的request或者response對象 37 ch.pipeline().addLast("http-encoder", 38 new HttpResponseEncoder());//響應解碼器 39 ch.pipeline().addLast("http-chunked", 40 new ChunkedWriteHandler());//目的是支持異步大文件傳輸() 41 ch.pipeline().addLast("fileServerHandler", 42 new HttpFileServerHandler(url));// 業務邏輯 43 } 44 }); 45 ChannelFuture future = b.bind("127.0.0.1", port).sync(); 46 System.out.println("HTTP文件目錄服務器啟動,網址是 : " + "http://127.0.0.1:" 47 + port + url); 48 future.channel().closeFuture().sync(); 49 } catch (Exception e) { 50 e.printStackTrace(); 51 } finally { 52 bossGroup.shutdownGracefully(); 53 workerGroup.shutdownGracefully(); 54 } 55 } 56 57 public static void main(String[] args) throws Exception { 58 int port = 8080; 59 if (args.length > 0) { 60 try { 61 port = Integer.parseInt(args[0]); 62 } catch (NumberFormatException e) { 63 e.printStackTrace(); 64 } 65 } 66 String url = DEFAULT_URL; 67 if (args.length > 1) 68 url = args[1]; 69 new HttpFileServer().run(port, url); 70 } 71 }
重點在於編解碼器,首先添加的HTTP請求消息解碼器HttpRequestDecoder,然后是HttpObjectAggregator解碼器,它的作用是將多個消息轉換為單一的FullHttpRequest或者FullHttpResponse,原因是HTTP解碼器在每個HTTP消息中會生成多個消息對象。
(1) HttpRequest/HttpResponse;
(2) HttpContent;
(3) LastHttpContent;
下面是FileServerHandler:
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.stream.ChunkedFile; import io.netty.util.CharsetUtil; import javax.activation.MimetypesFileTypeMap; import java.io.File; import java.io.FileNotFoundException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.regex.Pattern; import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * @author lilinfeng * @version 1.0 * @date 2014年2月14日 */ public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private final String url; public HttpFileServerHandler(String url) { this.url = url; } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { /*如果無法解碼400*/ if (!request.decoderResult().isSuccess()) { sendError(ctx, BAD_REQUEST); return; } /*只支持GET方法*/ if (request.method() != GET) { sendError(ctx, METHOD_NOT_ALLOWED); return; } final String uri = request.uri(); /*格式化URL,並且獲取路徑*/ final String path = sanitizeUri(uri); if (path == null) { sendError(ctx, FORBIDDEN); return; } File file = new File(path); /*如果文件不可訪問或者文件不存在*/ if (file.isHidden() || !file.exists()) { sendError(ctx, NOT_FOUND); return; } /*如果是目錄*/ if (file.isDirectory()) { //1. 以/結尾就列出所有文件 if (uri.endsWith("/")) { sendListing(ctx, file); } else { //2. 否則自動+/ sendRedirect(ctx, uri + '/'); } return; } if (!file.isFile()) { sendError(ctx, FORBIDDEN); return; } RandomAccessFile randomAccessFile = null; try { randomAccessFile = new RandomAccessFile(file, "r");// 以只讀的方式打開文件 } catch (FileNotFoundException fnfe) { sendError(ctx, NOT_FOUND); return; } long fileLength = randomAccessFile.length(); //創建一個默認的HTTP響應 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); //設置Content Length HttpUtil.setContentLength(response, fileLength); //設置Content Type setContentTypeHeader(response, file); //如果request中有KEEP ALIVE信息 if (HttpUtil.isKeepAlive(request)) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write(response); ChannelFuture sendFileFuture; //通過Netty的ChunkedFile對象直接將文件寫入發送到緩沖區中 sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise()); sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { if (total < 0) { // total unknown System.err.println("Transfer progress: " + progress); } else { System.err.println("Transfer progress: " + progress + " / " + total); } } @Override public void operationComplete(ChannelProgressiveFuture future) throws Exception { System.out.println("Transfer complete."); } }); ChannelFuture lastContentFuture = ctx .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //如果不支持keep-Alive,服務器端主動關閉請求 if (!HttpUtil.isKeepAlive(request)) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); if (ctx.channel().isActive()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); private String sanitizeUri(String uri) { try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { try { uri = URLDecoder.decode(uri, "ISO-8859-1"); } catch (UnsupportedEncodingException e1) { throw new Error(); } } if (!uri.startsWith(url)) { return null; } if (!uri.startsWith("/")) { return null; } uri = uri.replace('/', File.separatorChar); if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) { return null; } return System.getProperty("user.dir") + File.separator + uri; } private static final Pattern ALLOWED_FILE_NAME = Pattern .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*"); private static void sendListing(ChannelHandlerContext ctx, File dir) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); StringBuilder buf = new StringBuilder(); String dirPath = dir.getPath(); buf.append("<!DOCTYPE html>\r\n"); buf.append("<html><head><title>"); buf.append(dirPath); buf.append(" 目錄:"); buf.append("</title></head><body>\r\n"); buf.append("<h3>"); buf.append(dirPath).append(" 目錄:"); buf.append("</h3>\r\n"); buf.append("<ul>"); buf.append("<li>鏈接:<a href=\"../\">..</a></li>\r\n"); for (File f : dir.listFiles()) { if (f.isHidden() || !f.canRead()) { continue; } String name = f.getName(); if (!ALLOWED_FILE_NAME.matcher(name).matches()) { continue; } buf.append("<li>鏈接:<a href=\""); buf.append(name); buf.append("\">"); buf.append(name); buf.append("</a></li>\r\n"); } buf.append("</ul></body></html>\r\n"); ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); response.content().writeBytes(buffer); buffer.release(); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void sendRedirect(ChannelHandlerContext ctx, String newUri) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND); response.headers().set(HttpHeaderNames.LOCATION, newUri); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void setContentTypeHeader(HttpResponse response, File file) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); } }
上面的代碼注釋相對詳細,這里大致梳理一下。
(1) 不能解碼返回400,只支持GET請求,否則返回405.
(2) 對url包裝,使用UTF-8字符集,轉換為絕對path
(3) 如果是目錄,創建一個html頁面
(4) 如果是文件,設置content-type和content-length,使用netty的chunkedfile直接寫到緩沖,異步的方式
(5) 如果是非keepalived的,服務器端主動關閉,否則等待客戶端主動關閉。
三、Netty HTTP+Json協議棧開發
說明:原書是使用XML協議開發,XML框架用的是JiBX,這里用的是Json。
3.1 業務場景與協議設計
我們模擬一個簡單的用戶訂購系統。
訂購請求信息:
字段名稱 | 類型 | 備注 |
訂購數量 | Int64 | 訂購的商品數量 |
客戶信息 | Customer | 客戶信息,負責POJO對象 |
賬單地址 | Address | 賬單的地址 |
寄送方式 | Shipping | 枚舉類型如下: 普通郵寄 宅急送 國際郵遞 國內快遞 國際快遞 |
送貨地址 | Address | 送貨地址 |
總價 | float | 商品總價 |
客戶信息定義:
字段名稱 | 類型 | 備注 |
客戶ID | Int64 | 客戶ID,長整型 |
姓 | String | 客戶姓氏,字符串 |
名 | String | 客戶名字,字符串 |
全名 | List<String> | 客戶全稱,字符列表 |
地址信息:
字段名稱 | 類型 | 備注 |
街道1 | String | |
街道2 | String | |
城市 | String | |
省份 | String | |
郵編 | String | |
國家 | String |
郵遞方式:
字段名稱 | 類型 | 備注 |
普通郵遞 | 枚舉類型 | |
宅急送 | 枚舉類型 | |
國際郵遞 | 枚舉類型 | |
國內快遞 | 枚舉類型 | |
國際快遞 | 枚舉類型 |
流程設計如下:
- client端構造訂閱請求消息,將請求消息編碼為HTTP+json格式
- client端發起連接,通過HTTP協議棧發送HTTP請求消息
- server端對HTTP+json請求消息進行解碼,解碼成請求POJO
- server端構造應答消息並編碼,通過HTTP+json方式返回給客戶端
- client端對HTTP+json響應消息進行解碼,解碼成響應POJO
3.2 服務端編解碼器
涉及的類比較多:
netty開發的關鍵在於各種編解碼器。
首先定義自己的請求類和響應類:
import io.netty.handler.codec.http.FullHttpRequest; /** * @author Lilinfeng * @version 1.0 * @date 2014年3月1日 */ public class HttpJsonRequest { private FullHttpRequest request; private Object body; public HttpJsonRequest(FullHttpRequest request, Object body) { this.request = request; this.body = body; } /** * @return the request */ public final FullHttpRequest getRequest() { return request; } /** * @param request the request to set */ public final void setRequest(FullHttpRequest request) { this.request = request; } /** * @return the object */ public final Object getBody() { return body; } /** * @param object the object to set */ public final void setBody(Object body) { this.body = body; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return "HttpJsonRequest [request=" + request + ", body =" + body + "]"; } }
import io.netty.handler.codec.http.FullHttpResponse; /** * @author Administrator * @version 1.0 * @date 2014年3月1日 */ public class HttpJsonResponse { private FullHttpResponse httpResponse; private Object result; public HttpJsonResponse(FullHttpResponse httpResponse, Object result) { this.httpResponse = httpResponse; this.result = result; } /** * @return the httpResponse */ public final FullHttpResponse getHttpResponse() { return httpResponse; } /** * @param httpResponse the httpResponse to set */ public final void setHttpResponse(FullHttpResponse httpResponse) { this.httpResponse = httpResponse; } /** * @return the body */ public final Object getResult() { return result; } /** * @param body the body to set */ public final void setResult(Object result) { this.result = result; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return "HttpJsonResponse [httpResponse=" + httpResponse + ", result=" + result + "]"; } }
根據這2個類來設計流程,我們可以使用netty對http協議支持的編解碼器,首先我們使用了FastJson來作為json的框架,因此先定義2個抽象類,其中封裝了json的轉換方法,雖然看上去有點復雜,但是僅僅封裝了json化和反json化方法。
import demo.protocol.http.json.FastJsonUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageEncoder; import java.nio.charset.Charset; /** * Created by carl.yu on 2016/12/16. */ public abstract class AbstractHttpJsonEncoder<T> extends MessageToMessageEncoder<T> { final static Charset UTF_8 = Charset.forName("utf-8"); protected ByteBuf encode0(ChannelHandlerContext ctx, Object body) { String jsonStr = FastJsonUtils.convertObjectToJSON(body); ByteBuf encodeBuf = Unpooled.copiedBuffer(jsonStr, UTF_8); return encodeBuf; } }
import demo.protocol.http.json.FastJsonUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; import java.nio.charset.Charset; /** * Created by carl.yu on 2016/12/16. */ public abstract class AbstractHttpJsonDecoder<T> extends MessageToMessageDecoder<T> { private Class<?> clazz; private boolean isPrint; private final static Charset UTF_8 = Charset.forName("UTF-8"); protected AbstractHttpJsonDecoder(Class<?> clazz) { this(clazz, false); } protected AbstractHttpJsonDecoder(Class<?> clazz, boolean isPrint) { this.clazz = clazz; this.isPrint = isPrint; } protected Object decode0(ChannelHandlerContext ctx, ByteBuf body) { String content = body.toString(UTF_8); if (isPrint) System.out.println("The body is : " + content); Object result = FastJsonUtils.convertJSONToObject(content, clazz); return result; } }
服務端編解碼器: 獲取請求,最終解碼為自定義的HttpJsonRequest對象
(1) HttpRequestDecoder:請求消息解碼器,轉換為消息對象。
(2) HttpObjectAggregator: 目的是將多個消息轉換為單一的request或者response對象,最終得到的是FullHttpRequest對象
(3) 需要自定義的解碼器HttpJsonRequestDecoder,將FullHttpRequest轉換為HttpJsonRequest對象
服務器端編碼器:發送響應,將生成的數據轉換為DefaultFullHttpResponse對象發送出去.
(1) HttpResponseEncoder:響應消息編碼器,已經是一個HTTP消息了
(2) 自定義編碼器 HttpJsonResponseEncoder:由於Netty的DefaultFullHttpResponse沒有提供動態設置消息體content的接口。因此我們只能復制一個新的HTTP消息,將動態內容加入,生成一個DefaultFullHttpResponse對象。
上面涉及到的類如下:
import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.util.CharsetUtil; import java.util.List; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonRequestDecoder extends AbstractHttpJsonDecoder<FullHttpRequest> { public HttpJsonRequestDecoder(Class<?> clazz) { this(clazz, false); } /** * 構造器 * * @param clazz 解碼的對象信息 * @param isPrint 是否需要打印 */ public HttpJsonRequestDecoder(Class<?> clazz, boolean isPrint) { super(clazz, isPrint); } /** * @param ctx channel上下文 * @param msg 消息 * @param out 輸出集合 * @throws Exception */ @Override protected void decode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception { if (!msg.decoderResult().isSuccess()) { sendError(ctx, HttpResponseStatus.BAD_REQUEST); return; } HttpJsonRequest request = new HttpJsonRequest(msg, decode0(ctx, msg.content())); out.add(request); } /** * 測試的話,直接封裝,實戰中需要更健壯的處理 */ private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } }
/** * Created by carl.yu on 2016/12/16. */ public class HttpJsonResponseEncoder extends AbstractHttpJsonEncoder<HttpJsonResponse> { @Override protected void encode(ChannelHandlerContext ctx, HttpJsonResponse msg, List<Object> out) throws Exception { //編碼 ByteBuf body = encode0(ctx, msg.getResult()); FullHttpResponse response = msg.getHttpResponse(); if (response == null) { response = new DefaultFullHttpResponse(HTTP_1_1, OK, body); } else { response = new DefaultFullHttpResponse(msg.getHttpResponse() .protocolVersion(), msg.getHttpResponse().status(), body); } response.headers().set(CONTENT_TYPE, "text/json"); HttpUtil.setContentLength(response, body.readableBytes()); out.add(response); } }
3.3 客戶端編解碼器
客戶端解碼器:
- 使用netty自帶的HttpResponseDecoder和HttpObjectAggregator將響應解碼成FullHttpResponse
- 使用自定義的解碼器HttpJsonResponseDecoder封裝為使用的HttpJsonResponse
客戶端編碼器
- 使用netty自帶的HttpRequestEncoder寫入HttpJsonRequest
- 使用自定義的編碼器HttpJsonRequestEncoder封裝成FullHttpRequest然后發送
import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.*; import java.net.InetAddress; import java.util.List; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonRequestEncoder extends AbstractHttpJsonEncoder<HttpJsonRequest> { @Override protected void encode(ChannelHandlerContext ctx, HttpJsonRequest msg, List<Object> out) throws Exception { //(1)調用父類的encode0,將業務需要發送的對象轉換為Json ByteBuf body = encode0(ctx, msg.getBody()); //(2) 如果業務自定義了HTTP消息頭,則使用業務的消息頭,否則在這里構造HTTP消息頭 // 這里使用硬編碼的方式來寫消息頭,實際中可以寫入配置文件 FullHttpRequest request = msg.getRequest(); if (request == null) { request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/do", body); HttpHeaders headers = request.headers(); headers.set(HttpHeaderNames.HOST, InetAddress.getLocalHost() .getHostAddress()); headers.set(HttpHeaderNames.CONNECTION, HttpHeaders.Values.CLOSE); headers.set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP.toString() + ',' + HttpHeaderValues.DEFLATE.toString()); headers.set(HttpHeaderNames.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "zh"); headers.set(HttpHeaderNames.USER_AGENT, "Netty json Http Client side"); headers.set(HttpHeaderNames.ACCEPT, "text/html,application/json;q=0.9,*/*;q=0.8"); } HttpUtil.setContentLength(request, body.readableBytes()); // (3) 編碼后的對象 out.add(request); } }
import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpResponse; import java.util.List; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonResponseDecoder extends AbstractHttpJsonDecoder<FullHttpResponse> { public HttpJsonResponseDecoder(Class<?> clazz) { this(clazz, false); } /** * 構造器 * * @param clazz 解碼的對象信息 * @param isPrint 是否需要打印 */ public HttpJsonResponseDecoder(Class<?> clazz, boolean isPrint) { super(clazz, isPrint); } /** * @param ctx channel上下文 * @param msg 消息 * @param out 輸出集合 * @throws Exception */ @Override protected void decode(ChannelHandlerContext ctx, FullHttpResponse msg, List<Object> out) throws Exception { System.out.println("開始解碼..."); out.add( new HttpJsonResponse(msg, decode0(ctx, msg.content())) ); } }
3.4 開發HttpServer和HttpClient
server部分:
import demo.protocol.http.json.codec.HttpJsonRequestDecoder; import demo.protocol.http.json.codec.HttpJsonResponseEncoder; import demo.protocol.http.json.pojo.Order; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import java.net.InetSocketAddress; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonServer { public void run(final int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //接收HttpJsonRequest,需要對應解碼器 //ByteBuf->FullHttpRequest-> HttpJsonRequestDecoder //輸出HttpJsonResponse,需要對應編碼器 //HttpResponseEncoder->FullHttpResponse-> HttpJsonResponseEncoder ch.pipeline().addLast("http-decoder", new HttpRequestDecoder()); ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536)); ch.pipeline().addLast("json-decoder", new HttpJsonRequestDecoder(Order.class, true)); ch.pipeline().addLast("http-encoder", new HttpResponseEncoder()); ch.pipeline().addLast("json-encoder", new HttpJsonResponseEncoder()); ch.pipeline().addLast("jsonServerHandler", new HttpJsonServerHandler()); } }); ChannelFuture future = b.bind(new InetSocketAddress(port)).sync(); System.out.println("HTTP訂購服務器啟動,網址是 : " + "http://localhost:" + port); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } new HttpJsonServer().run(port); } }
import demo.protocol.http.json.codec.HttpJsonRequest; import demo.protocol.http.json.codec.HttpJsonResponse; import demo.protocol.http.json.pojo.Address; import demo.protocol.http.json.pojo.Order; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import java.util.ArrayList; import java.util.List; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonServerHandler extends SimpleChannelInboundHandler<HttpJsonRequest> { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpJsonRequest msg) throws Exception { HttpRequest request = msg.getRequest(); Order order = (Order) msg.getBody(); System.out.println("Http server receive request : " + order); dobusiness(order); ChannelFuture future = ctx.writeAndFlush(new HttpJsonResponse(null, order)); if (!HttpUtil.isKeepAlive(request)) { future.addListener(new GenericFutureListener<Future<? super Void>>() { public void operationComplete(Future future) throws Exception { ctx.close(); } }); } } private void dobusiness(Order order) { order.getCustomer().setFirstName("狄"); order.getCustomer().setLastName("仁傑"); List<String> midNames = new ArrayList<String>(); midNames.add("李元芳"); order.getCustomer().setMiddleNames(midNames); Address address = order.getBillTo(); address.setCity("洛陽"); address.setCountry("大唐"); address.setState("河南道"); address.setPostCode("123456"); order.setBillTo(address); order.setShipTo(address); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); if (ctx.channel().isActive()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("失敗: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } }
Client部分:
import demo.protocol.http.json.codec.HttpJsonRequestEncoder; import demo.protocol.http.json.codec.HttpJsonResponseDecoder; import demo.protocol.http.json.pojo.Order; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpResponseDecoder; import java.net.InetSocketAddress; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonClient { public void connect(int port) throws Exception { // 配置客戶端NIO線程組 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast("http-decoder", new HttpResponseDecoder()); ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536)); // json解碼器 ch.pipeline().addLast("json-decoder", new HttpJsonResponseDecoder(Order.class, true)); ch.pipeline().addLast("http-encoder", new HttpRequestEncoder()); ch.pipeline().addLast("json-encoder", new HttpJsonRequestEncoder()); ch.pipeline().addLast("jsonClientHandler", new HttpJsonClientHandler()); } }); // 發起異步連接操作 ChannelFuture f = b.connect(new InetSocketAddress(port)).sync(); // 當代客戶端鏈路關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放NIO線程組 group.shutdownGracefully(); } } /** * @param args * @throws Exception */ public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 采用默認值 } } new HttpJsonClient().connect(port); } }
import demo.protocol.http.json.codec.HttpJsonRequest; import demo.protocol.http.json.pojo.OrderFactory; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * Created by carl.yu on 2016/12/16. */ public class HttpJsonClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("連接上服務器..."); HttpJsonRequest request = new HttpJsonRequest(null, OrderFactory.create(123)); ctx.writeAndFlush(request); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println(msg.getClass().getName()); System.out.println("接收到了數據..." + msg); } /*@Override protected void channelRead0(ChannelHandlerContext ctx, HttpJsonResponse msg) throws Exception { System.out.println("The client receive response of http header is : " + msg.getHttpResponse().headers().names()); System.out.println("The client receive response of http body is : " + msg.getResult()); }*/ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
運行即可。