應用層協議http,發展至今已是http2.0了,擁有如下特色:html
(1) CS模式的協議java
(2) 簡單 - 只須要服務URL,攜帶必要的請求參數或者消息體web
(3) 靈活 - 任意類型,傳輸內容類型由HTTP消息頭中的Content-Type加以標記json
(4) 無狀態 - 必須藉助額外手段,好比session或者cookie來保持狀態bootstrap
客戶端發送一個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 方法。cookie
序號 | 方法 | 描述 |
---|---|---|
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 |
HTTP響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文。
當瀏覽者訪問一個網頁時,瀏覽者的瀏覽器會向網頁所在服務器發出請求。當瀏覽器接收並顯示網頁前,此網頁所在的服務器會返回一個包含HTTP狀態碼的信息頭(server header)用以響應瀏覽器的請求。
HTTP狀態碼的英文爲HTTP Status Code。
下面是常見的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 |
netty天生異步事件驅動的架構,不管是在性能上仍是在可靠性上,都表現優異,很是適合在非Web容器的場景下應用,相比於傳統的Tomcat,Jetty等Web容器,更加的輕量和小巧、靈活性和定製性也更好。
咱們以文件服務器爲例學習Netty的HTTP服務端入門開發,例程場景以下:
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的,服務器端主動關閉,不然等待客戶端主動關閉。
說明:原書是使用XML協議開發,XML框架用的是JiBX,這裏用的是Json。
咱們模擬一個簡單的用戶訂購系統。
字段名稱 | 類型 | 備註 |
訂購數量 | Int64 | 訂購的商品數量 |
客戶信息 | Customer | 客戶信息,負責POJO對象 |
帳單地址 | Address | 帳單的地址 |
寄送方式 | Shipping | 枚舉類型以下: 普通郵寄 宅急送 國際郵遞 國內快遞 國際快遞 |
送貨地址 | Address | 送貨地址 |
總價 | float | 商品總價 |
字段名稱 | 類型 | 備註 |
客戶ID | Int64 | 客戶ID,長整型 |
姓 | String | 客戶姓氏,字符串 |
名 | String | 客戶名字,字符串 |
全名 | List<String> | 客戶全稱,字符列表 |
字段名稱 | 類型 | 備註 |
街道1 | String | |
街道2 | String | |
城市 | String | |
省份 | String | |
郵編 | String | |
國家 | String |
字段名稱 | 類型 | 備註 |
普通郵遞 | 枚舉類型 | |
宅急送 | 枚舉類型 | |
國際郵遞 | 枚舉類型 | |
國內快遞 | 枚舉類型 | |
國際快遞 | 枚舉類型 |
涉及的類比較多:
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; } }
(1) HttpRequestDecoder:請求消息解碼器,轉換爲消息對象。
(2) HttpObjectAggregator: 目的是將多個消息轉換爲單一的request或者response對象,最終獲得的是FullHttpRequest對象
(3) 須要自定義的解碼器HttpJsonRequestDecoder,將FullHttpRequest轉換爲HttpJsonRequest對象
(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); } }
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())) ); } }
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); } }
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(); } }
運行便可。