Netty系列文章之構建HTTP(HTTPS)應用程式
這篇文章主要介紹如何用Netty構建一個HTTP/HTTPS應用程式,用一個HelloWorld級Demo進行闡述
SSL/TLS協議簡介
因為要同時構建HTTPS應用程式,所以我們需要通過使用 SSL/TLS保護Netty應用程式,這裡先簡單介紹下 SSL/TLS協議。
SSL和TLS都是運輸層的安全協議, 它們發展歷史如下:
- 1995: SSL 2.0 ,由Netscape提出,這個版本由於設計缺陷,並不安全,很快被發現有嚴重漏洞,已經廢棄
- 1996:SSL 3.0寫成RFC,開始流行,目前(從2015年)已經不安全,必須禁用
- 1999:TLS1.0網際網路標準化組織ISOC接替NetScape公司,釋出了 SSL 的升級版TLS1.0版
- 2006: TLS 1.1. 作為 RFC 4346 釋出。主要fix了CBC模式相關的如BEAST攻擊等漏洞
- 2008: TLS 1.2. 作為RFC 5246 釋出 。增進安全性。目前(2015年)應該主要部署的版本,請確保你使用的是這個版本
- 2015之後: TLS 1.3,還在制訂中,支援0-rtt,大幅增進安全性,砍掉了aead之外的加密方式
由於SSL的2個版本都已經退出歷史舞臺, 現在一般所說的SSL就是TLS
SSL/TLS安全協議示意圖如下:

SSL/TLS協議是一個位於HTTP層與TCP層之間的可選層,其提供的服務主要有:
- 認證使用者和伺服器,確保資料傳送到正確的客戶機和伺服器
- 加密資料以防止資料中途被竊取
- 維護資料的完整性,確保資料在傳輸過程中不被改變
關於SSL/TLS協議更加詳細的介紹可以查詢相關資料,這裡就不細說了。
JDK的javax.net.ssl包 VS Netty的OpenSSL/SSLEngine
為了支援 SSL/TLS,Java提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 類使得解密和加密相當簡單和高效。SSLContext是SSL連結的上下文,SSLEngine主要用於出站和入站位元組流的操作。
Netty還提供了使用 OpenSSL工具包的SSLEngine實現,該SSLEngine比JDK提供的SSLEngine實現有更好的效能
Netty通過一個名為 SslHandler
的 ChannelHandler
實現加密和解密的功能,其中 SslHandler
在內部使用SSLEngine來完成實際的工作,SSLEngine的實現可以是JDK的 SSLEngine
,也可以是 Netty 的 OpenSslEngine
,當然推薦使用Netty的OpenSslEngine,因為它效能更好,通過SslHandler進行解密和加密的過程如下圖所示(摘自《Netty In Action》):

大多數情況下,SslHandler 將是 ChannelPipeline 中的第一個 ChannelHandler。這確保了只有在所有其他的 ChannelHandler 將它們的邏輯應用到資料之後,才會進行加密。
HTTP請求和響應組成部分
HTTP是基於請求/響應模型的的: 客戶端向服務端傳送一個HTTP請求,然後服務端將會返回一個HTTP響應,Netty提供了多種編碼器和解碼器以簡化對這個協議的使用。
HTTP請求的組成部分如下圖:

HTTP響應的組成部分如下圖:

如上面兩圖所示,一個HTTP請求/響應可能由多個數據部分組成,並且它總是以一個 LastHttpContent 部分作為結束。 FullHttpRequest
和 FullHttpResponse
訊息是特殊的子型別,分別代表了完整的請求和響應。
所有型別的HTTP訊息都實現了 HttpObject
介面
HTTP解碼器、編碼器和編解碼器
Netty為HTTP訊息提供了編碼器和解碼器:
HttpRequestEncoder HttpResponseEecoder HttpRequestDecoder HttpResponseDecoder
編解碼器:
-
HttpClientCodec
: 用於客戶端的編解碼器,等效於HttpRequestEncoder
和HttpResponseDecoder
的組合 -
HttpServerCodec
:用於服務端的編解碼器,等效於HttpRequsetDecoder
和HttpResponseEncoder
的組合
以 HttpServerCodec
為例,它的類繼承結構圖如下:

HttpServerCodec 同時實現了 ChannelInboundHandler
和 ChannelOutboundHandler
介面,以達到同時具有編碼和解碼的能力。
聚合器:
-
HttpObjectAggregator
: 聚合器,可以將多個訊息部分合併為FullHttpRequest
或者FullHttpResponse
訊息。使用該聚合器的原因是HTTP解碼器會在每個HTTP訊息中生成多個訊息物件,如HttpRequest/HttpResponse,HttpContent,LastHttpContent
,使用聚合器將它們聚合成一個完整的訊息內容,這樣就不用關心訊息碎片了。
應用程式程式碼
構建基於Netty的HTTP/HTTPS 應用程式的原始碼出自於Netty官方提供的demo,我略微做了一些改動,原地址是: ofollow,noindex">github.com/netty/netty…
原始碼:
public class HttpHelloWorldServer { static final boolean SSL = System.getProperty("ssl") != null; static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080")); public static void main(String[] args) throws Exception { final SslContext sslContext; //判斷SSL是否為true,為true表示使用HTTPS連線,反之,使用HTTP if (SSL) { //使用Netty自帶的證書工具生成一個數字證書 SelfSignedCertificate certificate = new SelfSignedCertificate(); sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build(); } else { sslContext = null; } EventLoopGroup boss = new NioEventLoopGroup(1); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (sslContext != null) { pipeline.addLast(sslContext.newHandler(ch.alloc())); } //新增一個HTTP的編解碼器 pipeline.addLast(new HttpServerCodec()); //新增HTTP訊息聚合器 pipeline.addLast(new HttpObjectAggregator(64 * 1024)); //新增一個自定義服務端Handler pipeline.addLast(new HttpHelloWorldServerHandler()); } }); ChannelFuture future = bootstrap.bind(PORT).sync(); System.err.println("Open your web browser and navigate to " + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); future.channel().closeFuture().sync(); } finally { boss.shutdownGracefully().sync(); worker.shutdownGracefully().sync(); } } } 複製程式碼
程式碼解讀
首先判斷系統屬性ssl是否存在,如果存在,則表明使用安全連線,反之,則使用一般的HTTP連線。
final SslContext sslContext; if (SSL) { SelfSignedCertificate certificate = new SelfSignedCertificate(); sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build(); } else { sslContext = null; } 複製程式碼
上面程式碼所示,當SSL為true時,使用Netty自帶的簽名證書工具自定義服務端傳送給客戶端的數字證書。
接下來和一般的Netty服務端程式步驟一樣,先建立 ServerBootstrap
啟動類,設定和繫結 NioEventLoopGroup
執行緒池,建立服務端 Channel,新增ChannelHandler。值得注意的是,新增的ChannelHandler都是與HTTP相關的Handler。
HttpHelloWorldServerHandler
自定義的Handler程式碼如下:
public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> { private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type"); private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length"); private static final AsciiString CONNECTION = AsciiString.cached("Connection"); private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive"); @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; System.out.println("瀏覽器請求方式:"+req.method().name()); String content = ""; if ("/hello".equals(req.uri())) { content = "hello world"; response2Client(ctx,req,content); } else { content = "Connect the Server"; response2Client(ctx,req,content); } } } private void response2Client(ChannelHandlerContext ctx, HttpRequest req, String content) { boolean keepAlive = HttpUtil.isKeepAlive(req); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes())); response.headers().set(CONTENT_TYPE, "text/plain"); response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); if (!keepAlive) { ctx.write(response).addListener(ChannelFutureListener.CLOSE); } else { response.headers().set(CONNECTION, KEEP_ALIVE); ctx.write(response); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } 複製程式碼
在此Handler中處理入站資料流,但該程式碼只是處理 GET
請求,沒有對 POST
請求做出處理,所以當瀏覽器傳送一個 GET
請求時,此Handler定義一個HTTP響應體 FullHttpResponse
,設定一些響應頭,如· Content-type
、 Connection
、 Content-Length
等,設定響應內容,然後通過 ctx.write
方法寫入HTTP訊息
AsciiString
在設定響應頭時我們用到了 AsciiString ,從Netty 4.1開始,提供了實現了 CharSequence
介面的 AsciiString
,至於 CharSequence
就是 String
的父類。 AsciiString
包含的字元只佔1個位元組,當你處理 US-ASCII 或者 ISO-8859-1 字串時可以節省空間。例如,HTTP編解碼器使用 AsciiString
處理 header name ,因為將 AsciiString
編碼到 ByteBuf
中不會有型別轉換的代價,其內部實現就是用的 byte
,而對於 String
來說,內部是存 char[]
,使用 String就需要將 char轉換成 byte,所以 AsciiString
比String型別有更好的效能。