原文博客地址: pjmike的博客html
這篇文章主要介紹如何用Netty構建一個HTTP/HTTPS應用程序,用一個HelloWorld級Demo進行闡述java
由於要同時構建HTTPS應用程序,因此咱們須要經過使用 SSL/TLS保護Netty應用程序,這裏先簡單介紹下 SSL/TLS協議。git
SSL和TLS都是運輸層的安全協議, 它們發展歷史以下:github
因爲SSL的2個版本都已經退出歷史舞臺,如今通常所說的SSL就是TLSweb
SSL/TLS安全協議示意圖以下:bootstrap
SSL/TLS協議是一個位於HTTP層與TCP層之間的可選層,其提供的服務主要有:瀏覽器
關於SSL/TLS協議更加詳細的介紹能夠查找相關資料,這裏就不細說了。安全
爲了支持 SSL/TLS,Java提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 類使得解密和加密至關簡單和高效。SSLContext是SSL連接的上下文,SSLEngine主要用於出站和入站字節流的操做。服務器
Netty還提供了使用 OpenSSL工具包的SSLEngine實現,該SSLEngine比JDK提供的SSLEngine實現有更好的性能app
Netty經過一個名爲SslHandler
的ChannelHandler
實現加密和解密的功能,其中SslHandler
在內部使用SSLEngine來完成實際的工做,SSLEngine的實現能夠是JDK的SSLEngine
,也能夠是 Netty 的OpenSslEngine
,固然推薦使用Netty的OpenSslEngine,由於它性能更好,經過SslHandler進行解密和加密的過程以下圖所示(摘自《Netty In Action》):
大多數狀況下,SslHandler 將是 ChannelPipeline 中的第一個 ChannelHandler。這確保了只有在全部其餘的 ChannelHandler 將它們的邏輯應用到數據以後,纔會進行加密。
HTTP是基於請求/響應模型的的: 客戶端向服務端發送一個HTTP請求,而後服務端將會返回一個HTTP響應,Netty提供了多種編碼器和解碼器以簡化對這個協議的使用。
HTTP請求的組成部分以下圖:
HTTP響應的組成部分以下圖:
如上面兩圖所示,一個HTTP請求/響應可能由多個數據部分組成,而且它老是以一個 LastHttpContent 部分做爲結束。 FullHttpRequest
和FullHttpResponse
消息是特殊的子類型,分別表明了完整的請求和響應。
全部類型的HTTP消息都實現了 HttpObject
接口
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,我略微作了一些改動,原地址是:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld
源代碼:
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。
自定義的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,從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官方還提供了不少其餘方面的例子,對於入門學習來講還不錯,詳細地址是: https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example