原文博客地址: 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