原文部落格地址: pjmike的部落格
前言
這篇文章主要介紹如何用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,我略微做了一些改動,原地址是: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型別有更好的效能。
測試
客戶端測試:
服務端日誌:
小結
以上總結了如何使用Netty構建一個簡單的HTTP/HTTPS應用程式。當然上面的程式參考的是Netty官方提供的Demo,Netty官方還提供了很多其他方面的例子,對於入門學習來說還不錯,詳細地址是: github.com/netty/netty…