通过SSL/TLS保护Netty应用程序
为了支持SSL/TLS,Java提供了javax.net.ssl包,它的SSLContext和SSLEngine类使得实现解密和加密相当简单直接。Netty通过一个名为SslHandler的ChannelHandler实现利用了这个API,其中SslHandler在内部使用SSLEngine来完成实际的工作。
Netty的OpenSSL/SSLEngine实现
Netty还提供了使用OpenSSL工具包(www.openssl.org)的SSLEngine实现。这个OpenSsl-Engine类提供了比JDK 提供的SSLEngine实现更好的性能。如果OpenSSL库可用,可以将Netty应用程序(客户端和服务器)配置为默认使用OpenSslEngine。如果不可用,Netty将会回退到JDK 实现。无论你使用JDK 的SSLEngine还是使用Netty的OpenSslEngine,SSL API和数据流都是一致的。
public class SslChannelInitializer extends ChannelInitializer<Channel> {private final SslContext context;private final boolean startTls;public SslChannelInitializer(SslContext context, boolean startTls)//传入要使用的SslContext{this.context = context;this.startTls = startTls;//如果设置为true,第一个写入的消息将不会被加密(客户端应该设置为true)}@Overrideprotected void initChannel(Channel ch) throws Exception{SSLEngine engine = context.newEngine(ch.alloc());//对于每个SslHandler实例,都使用Channel的ByteBuf-Allocator从SslContext获取一个新的SSLEnginech.pipeline().addFirst("ssl", new SslHandler(engine, startTls));//将SslHandler作为第一个ChannelHandler添加到ChannelPipeline中}
}
在大多数情况下,SslHandler将是ChannelPipeline中的第一个ChannelHandler。这确保了只有在所有其他的ChannelHandler将它们的逻辑应用到数据之后,才会进行加密。
方法名称 |
描述 |
setHandshakeTimeout (long , TimeUnit) setHandshakeTimeoutMillis (long) getHandshakeTimeoutMillis () |
设置和获取超时事件,超时后,握手ChannelFuture 将会被通知失败 |
setCloseNotifyTimeout (long, TimeUnit) setCloseNotifyTimeoutMillis (long) getCloseNotifyTimeoutMillis () |
设置和获取超时时间,超时后,将会触发一个关闭通知并关闭连接。同时会导致通知该ChannelFuture 失败 |
handshakeFuture() |
返回一个在握手完成后将会得到通知的ChannelFuture,如果握手前已经执行过,则返回包含了先前握手结果的ChannelFuture |
close() close(ChannelPromise) close(ChannelHandlerContext, ChannelPromise) |
发送 close_notify 以请求关闭并销毁底层的SslEngine |
构建基于Netty的HTTP/HTTPS应用程序
HTTP解码器、编码器和编解码器
HTTP是基于请求/响应模式的:客户端向服务器发送一个HTTP请求,然后服务器将会返回一个HTTP响应。Netty提供了多种编码器和解码器以简化对这个协议的使用。
一个HTTP请求/响应可能由多个数据部分组成,并且它总是以一个LastHttpContent部分作为结束。FullHttpRequest和FullHttpResponse消息是特殊的子类型,分别代表了完整的请求和响应。所有类型的HTTP消息(FullHttpRequest、LastHttpContent以及代码清单中展示的那些)都实现了HttpObject接口。
HTTP解码器和编码器:
名称 | 描述 |
HttpRequestEncoder | 将HttpRequest、HttpContent和LastHttpContent消息编码为字节 |
HttpResponseEncoder | 将HttpResponse、HttpContent和LastHttpContent消息编码为字节 |
HttpRequestDecoder | 将字节解码为HttpRequest、HttpContent和LastHttpContent消息 |
HttpResponseDecoder | 将字节解码为HttpResponse、HttpContent和LastHttpContent消息 |
public class HttpPipelineInitializer extends ChannelInitializer<Channel>
{private final boolean client;public HttpPipelineInitializer(boolean client){this.client = client;}@Overrideprotected void initChannel(Channel ch) throws Exception{ChannelPipeline pipeline = ch.pipeline();if (client) {pipeline.addLast("decoder", new HttpResponseDecoder());//如果是客户端,则添加HttpResponseDecoder以处理来自服务器的响应pipeline.addLast("encoder", new HttpRequestEncoder());//如果是客户端,则添加HttpRequestEncoder以向服务器发送请求} else {pipeline.addLast("decoder", new HttpRequestDecoder());//如果是服务器,则添加HttpRequestDecoder以接收来自客户端的请求pipeline.addLast("encoder", new HttpResponseEncoder());//如果是服务器,则添加HttpResponseEncoder以向客户端发送响应}}
}
聚合 HTTP消息
由于HTTP的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty提供了一个聚合器,它可以将多个消息部分合并为FullHttpRequest或者FullHttpResponse消息。通过这样的方式,你将总是看到完整的消息内容。
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {private final boolean isClient;public HttpAggregatorInitializer(boolean isClient) {this.isClient = isClient;}@Overrideprotected void initChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();if (isClient) {pipeline.addLast("codec", new HttpClientCodec());//如果是客户端,则添加HttpClientCodec} else {pipeline.addLast("codec", new HttpServerCodec());//如果是服务器,则添加HttpServerCodecs}pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));//将最大的消息大小为512KB的HttpObjectAggregator添加到ChannelPipeline }
}
HTTP压缩
HTTP请求的头部信息
客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式:
GET /encrypted-areaHTTP/1.1
Host:www.example.com
Accept-Encoding:gzip,deflate
public class HttpCompressionInitializer extends ChannelInitializer<Channel> {private final boolean isClient;public HttpCompressionInitializer(boolean isClient) {this.isClient = isClient;}@Overrideprotected void initChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();if (isClient) {pipeline.addLast("codec ", new HttpClientCodec());//如果是客户端,则添加HttpClientCodecpipeline.addLast("decompressor", new HttpContentDecompressor());//如果是客户端,则添加HttpContentDecompressor以处理来自服务器的压缩内容} else {pipeline.addLast("codec", new HttpServerCodec())//如果是服务器,则添加HttpServerCodec;pipeline.addLast("compressor", new HttpContentCompressor());//如果是服务器,则添加HttpContentCompressor来压缩数据(如果客户端支持它)}}
}
使用 HTTPS
启用HTTPS只需要将SslHandler添加到ChannelPipeline的ChannelHandler组合中。
public class HttpsCodecInitializer extends ChannelInitializer<Channel> {private final SslContext context;private final boolean isClient;public HttpsCodecInitializer(SslContext context, boolean isClient)//传入要使用的SslContext{this.context = context;this.isClient = isClient;}@Overrideprotected void initChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();SSLEngine engine = context.newEngine(ch.alloc());pipeline.addFirst("ssl", new SslHandler(engine));//将SslHandler添加到ChannelPipeline中以使用HTTPSif (isClient) {pipeline.addLast("codec", new HttpClientCodec());//如果是客户端,则添加HttpClientCodec} else {pipeline.addLast("codec", new HttpServerCodec());//如果是服务器,则添加HttpServerCodec}}
}
WebSocket
要想向你的应用程序中添加对于WebSocket的支持,你需要将适当的客户端或者服务器WebSocketChannelHandler添加到ChannelPipeline中。这个类将处理由WebSocket定义的称为帧的特殊消息类型。
名称 | 描述 |
BinaryWebSocketFrame | 数据帧:二进制数据 |
TextWebSocketFrame | 数据帧:文本数据 |
ContinuationWebSocketFrame | 数据帧:属于上一个BinaryWebSocketFrame或者TextWeb-SocketFrame的文本的或者二进制数据 |
CloseWebSocketFrame | 控制帧:一个CLOSE请求、关闭的状态码以及关闭的原因 |
PingWebSocketFrame | 控制帧:请求一个PongWebSocketFrame |
PongWebSocketFrame | 控制帧:对PingWebSocketFrame请求的响应 |
使用WebSocketServerProtocolHandler的简单示例,这个类处理协议升级握手,以及3种控制帧——Close、Ping和Pong。Text和Binary数据帧将会被传递给下一个(由你实现的)ChannelHandler进行处理。
public class WebSocketServerInitializer extends ChannelInitializer<Channel> {@Overrideprotected void initChannel(Channel ch) throwsException {ch.pipeline().addLast(new HttpServerCodec(),new HttpObjectAggregator(65536),//为握手提供聚合的HttpRequestnew WebSocketServerProtocolHandler("/websocket"),//如果被请求的端点是"/websocket",则处理该升级握手new TextFrameHandler(),//TextFrameHandler处理TextWebSocketFramenew BinaryFrameHandler(),//BinaryFrameHandler处理BinaryWebSocketFramenew ContinuationFrameHandler());//ContinuationFrameHandler处理ContinuationWebSocketFrame}public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {@Overridepublic voidchannelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {// Handle text frame}}public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {@Overridepublic voidchannelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {// Handle binary frame}}public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> {@Overridepublic voidchannelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {// Handle continuation frame}}
}
空闲的连接和超时
检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty特地为它提供了几个ChannelHandler实现。
名称 |
描述 |
IdleStateHandler |
当链接空闲时间太长,将触发一个IdleStateEvent 事件。然后,可以通过 ChannelInboundHandler中重写userEventTriggered()方法来处理该 IdleStateEvent 事件 |
ReadTimeoutHandler |
如果指定时间间隔内没收到任何入站数据,则抛出一个ReadTimeoutException 并关闭对应Channel。可以重写ChannelHandler中的exceptionCaught()方法检测该ReadTimeoutException |
WriteTimeoutHandler |
如果指定时间间隔内没有任何出站数据写入,则抛出WriteTimeoutExceptoin 并关闭对应Channel。可以重写ChannelHandler 的exceptionCaught()方法检测该WriteTimeout-Exception |
如果在60秒之内没有接收或者发送任何的数据,我们将如何得到通知;如果没有响应,则连接会被关闭:
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {@Overrideprotected void initChannel(Channel ch) throwsException {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));//IdleStateHandler将在被触发时发送一个IdleStateEvent事件pipeline.addLast(new HeartbeatHandler());//将一个HeartbeatHandler添加到Chan-nelPipeline中pipeline.addLast(new MessageHander());}public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {实现userEventTriggered()方法以发送心跳消息private static final ByteBuf HEARTBEAT_SEQUENCE =Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));//发送到远程节点的心跳消息@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent) {ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);//发送心跳消息,并在发送失败时关闭该连接} else {super.userEventTriggered(ctx, evt);//不是IdleStateEvent事件,所以将它传递给下一个ChannelInboundHandler}}}public static final class MessageHander extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String body = new String(req, "UTF-8");System.out.println(body);}}
}
如果连接超过60秒没有接收或者发送任何的数据,那么IdleStateHandler将会使用一个IdleStateEvent事件来调用fireUserEventTriggered()方法。HeartbeatHandler实现了userEventTriggered()方法,如果这个方法检测到IdleStateEvent事件,它将会发送心跳消息,并且添加一个将在发送操作失败时关闭该连接的ChannelFutureListener。
解码基于分隔符的协议和基于长度的协议
基于分隔符的协议
基于分隔符的(delimited)消息协议使用定义的字符来标记的消息或者消息段(通常被称为帧)的开头或者结尾。
名称 |
描述 |
DelimiterBasedFrameDecoder |
使用任何由用户提供的分隔符来提取帧的通用解码器 |
LineasedFrameDecoder |
提取由行尾符(\n 或者 \r\n)分割的帧的解码器。这个解码器比DelimiterBaseFrameDecoder 更快 |
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {@Overrideprotected void initChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));//该LineBasedFrame-Decoder将提取的帧转发给下一个ChannelInboundHandlerpipeline.addLast(new FrameHandler());//添加FrameHandler以接收帧}public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overridepublic void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {// Do something with the data extracted from the frame}}
}
如果你正在使用除了行尾符之外的分隔符分隔的帧,那么你可以以类似的方式使用DelimiterBasedFrameDecoder,只需要将特定的分隔符序列指定到其构造函数即可。
这些解码器是实现你自己的基于分隔符的协议的工具。作为示例,我们将使用下面的协议规范:
- 传入数据流是一系列的帧,每个帧都由换行符(\n)分隔;
- 每个帧都由一系列的元素组成,每个元素都由单个空格字符分隔;
- 一个帧的内容代表一个命令,定义为一个命令名称后跟着数目可变的参数。
基于长度的协议
基于长度的协议通过将它的长度编码到帧的头部来定义帧,而不是使用特殊的分隔符来标记它的结束。
名称 |
描述 |
FixedLengthFrameDecoder |
提取在调用构造函数时指定的定长帧 |
LengthFieldBaseFrameDecoder |
根据编码进帧头部中的长度值提取帧; 该字段的偏移量以及长度在构造函数中指定 |
遇到被编码到消息头部的帧大小不是固定值的协议。为了处理这种变长帧,你可以使用LengthFieldBasedFrameDecoder,它将从头部字段确定帧长,然后从数据流中提取指定的字节数。
public class LengthBasedInitializer extends ChannelInitializer<Channel> {@Overrideprotected void initChannel(Channel ch) throwsException {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new LengthFieldBasedFrameDecoder(64 * 1024,0, 8));//使用LengthFieldBasedFrameDecoder解码将帧长度编码到帧起始的前8个字节中的消息pipeline.addLast(new FrameHandler());//添加FrameHandler以处理每个帧}public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overridepublic void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {// Do something with the frame}}
}
写大型数据
在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。NIO的零拷贝特性,这种特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在Netty的核心中,所以应用程序所有需要做的就是使用一个FileRegion接口的实现,其在Netty的API文档中的定义是:“通过支持零拷贝的文件传输的Channel来发送的文件区域。”
FileInputStream in = new FileInputStream(file);FileRegion region = new DefaultFileRegion( in.getChannel(), 0, file.length());//以该文件的完整长度创建一个新的DefaultFileRegionchannel.writeAndFlush(region).addListener(//发送该DefaultFile-Region,并注册一个ChannelFutureListenernew ChannelFutureListener() {@Overridepublic voidoperationComplete(ChannelFuture future)throws Exception {if (!future.isSuccess()) {Throwable cause = future.cause();// Do something }}});
这个示例只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。在需要将数据从文件系统复制到用户内存中时,可以使用ChunkedWriteHandler,它支持异步写大型数据流,而又不会导致大量的内存消耗。关键是interface ChunkedInput<B>,其中类型参数B是readChunk()方法返回的类型。Netty预置了该接口的4个实现。每个都代表了一个将由ChunkedWriteHandler处理的不定长度的数据流。
名称 | 描述 |
ChunkedFile | 从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用 |
ChunkedNioFile | 和ChunkedFile类似,只是它使用了FileChannel |
ChunkedStream | 从InputStream中逐块传输内容 |
ChunkedNioStream | 从ReadableByteChannel中逐块传输内容 |
当Channel的状态变为活动的时,WriteStreamHandler将会逐块地把来自文件中的数据作为ChunkedStream写入。数据在传输之前将会由SslHandler加密。
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {private final File file;private final SslContext sslCtx;public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {this.file = file;this.sslCtx = sslCtx;}@Overrideprotected voidinitChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new SslHandler(sslCtx.newEngine(ch.alloc())));//将SslHandler添加到ChannelPipeline中pipeline.addLast(new ChunkedWriteHandler());//添加Chunked-WriteHandler以处理作为ChunkedInput传入的数据pipeline.addLast(new WriteStreamHandler());//一旦连接建立,WriteStreamHandler就开始写文件数据}public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//当连接建立时,channelActive()方法将使用ChunkedInput写文件数据super.channelActive(ctx);ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));}}
}
序列化数据
JDK提供了ObjectOutputStream和ObjectInputStream,用于通过网络对POJO的基本数据类型和图进行序列化和反序列化。该API并不复杂,而且可以被应用于任何实现了java.io.Serializable接口的对象。但是它的性能也不是非常高效的。
名称 |
描述 |
CompatibleObjectDecoder |
和使用JDK序列化的 非基于Netty的 远程节点 进行互操作的解码器 |
CompatibleObjectEncode |
和使用JDK序列化的 非基于Netty的 远程节点 进行互操作的编码器 |
ObjectDecoder |
构建于JDK 序列化之上的使用自定义的序列化来解码的解码器; 当没有其他的外部依赖时,它提供了速度上的改进。否则其他序列化更可取 |
ObjectEncoder |
构建于JDK序列化之上的使用自定义的序列化来编码的编码器; 当没有其他的外部依赖时,它提供了速度上的改进。否则其他序列化更可取 |
使用 JBoss Marshalling进行序列化
它比JDK序列化最多快3倍,而且也更加紧凑。
名称 |
描述 |
CompatibleMarchallingDecoder CompatibleMarshallingEncoder |
与只使用JDK序列化的远程节点兼容 |
MarshallingDecoder MarshallingEncoder |
适用于使用JBoss Marshalling 的节点。这些类必须一起使用 |
import io.netty.channel.*;
import io.netty.handler.codec.marshalling.MarshallerProvider;
import io.netty.handler.codec.marshalling.MarshallingDecoder;
import io.netty.handler.codec.marshalling.MarshallingEncoder;
import io.netty.handler.codec.marshalling.UnmarshallerProvider;import java.io.Serializable;public class MarshallingInitializer extends ChannelInitializer<Channel> {private final MarshallerProvider marshallerProvider;private final UnmarshallerProvider unmarshallerProvider;public MarshallingInitializer(UnmarshallerProvider unmarshallerProvider, MarshallerProvider marshallerProvider) {this.marshallerProvider = marshallerProvider;this.unmarshallerProvider = unmarshallerProvider;}@Overrideprotected void initChannel(Channel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(new MarshallingDecoder(unmarshallerProvider));//添加MarshallingDecoder以将ByteBuf转换为POJOpipeline.addLast(new MarshallingEncoder(marshallerProvider));//添加Marshalling-Encoder以将POJO转换为ByteBufpipeline.addLast(new ObjectHandler());//添加ObjectHandler,以处理普通的实现了Serializable接口的POJO}public static final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {@Overridepublic void channelRead0(ChannelHandlerContext channelHandlerContext, Serializable serializable) throws Exception {// Do something}}
}
通过 Protocol Buffers 序列化
Netty序列化的最后一个解决方案是利用Protocol Buffers的编解码器,它是一种由Google公司开发的、现在已经开源的数据交换格式。
名称 |
描述 |
ProtobufDecoder |
使用 protobuf 对消息进行解码 |
ProtobufEncoder |
使用 protobuf 对消息进行编码 |
ProtobufVarint32FrameDecoder |
根据消息中的Google Protocol Buffer 的 “Base 128 Varints” 整型长度字段值动态地分割所接收到的ByteBuf |
ProtobufVarint32LengthFieldPrepender |
向ByteBuf 前追加一个Google Protocal Buffer 的“Base 128 Varints” 整型的长度字段值 |
import com.google.protobuf.MessageLite;
import io.netty.channel.*;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;public class ProtoBufInitializer extends ChannelInitializer<Channel> {private final MessageLite lite;public ProtoBufInitializer(MessageLite lite) {this.lite = lite;}@Overrideprotected void initChannel(Channel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new ProtobufVarint32FrameDecoder());//添加ProtobufVarint32FrameDecoder以分隔帧pipeline.addLast(new ProtobufEncoder());//添加ProtobufEncoder以处理消息的编码pipeline.addLast(new ProtobufDecoder(lite));//添加ProtobufDecoder以解码消息pipeline.addLast(new ObjectHandler());//添加Object-Handler以处理解码消息}public static final class ObjectHandler extends SimpleChannelInboundHandler<Object> {@Overridepublic void channelRead0(ChannelHandlerContextctx, Object msg) throws Exception {// Do something with the object}}
}
参考《Netty实战》