Netty实战 学习笔记
编解码器框架
什么是 Codec
编写一个网络应用程序需要实现某种 codec (编解码器),codec的作用就是将原始字节数据与目标程序数据格式进行互转。网络中都是以字节码的数据形式来传输数据的,codec 由两部分组成:decoder(解码器)和encoder(编码器)。
解码器负责将消息从字节或其他序列形式转成指定的消息对象,编码器则相反;
- 解码器负责处理inbound(入站)数据,
- 编码器负责处理outbound(出站)数据。
Decoder(解码器)
decoder 负责将“入站”数据从一种格式转换到另一种格式,Netty的解码器是一种ChannelInboundHandler 的抽象实现。实践中使用解码器很简单,就是将入站数据转换格式后传递到 ChannelPipeline 中的下一个ChannelInboundHandler 进行处理;
Netty 提供了丰富的解码器抽象基类,我们可以很容易的实现这些基类来自定义解码器。主要分两类:
- 解码字节到消息(ByteToMessageDecoder和ReplayingDecoder)
- 解码消息到消息(MessageToMessageDecoder)
ByteToMessageDecoder
ByteToMessageDecoder 是用于将字节转为消息(或其他字节序列).由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
假设你接收了一个包含简单 int 的字节流,每个 int都需要被单独处理。在这种情况下,你需要从入站 ByteBuf 中读取每个 int,并将它传递给ChannelPipeline 中的下一个 ChannelInboundHandler。为了解码这个字节流,你要扩展ByteToMessageDecoder 类。
该设计如下图:
代码如下:
//扩展 ByteToMessageDecoder 类,以将字节解码为特定的格式
@ChannelHandler.Sharable
class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if(in.readableBytes() >= 4) { //检查是否至少有 4字节可读(一个 int的字节长度)
out.add(in.readInt()); //从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
}
}
}
ReplayingDecoder
ReplayingDecoder 是 byte-to-message 解码的一种特殊的抽象基类,使用ReplayingDecoder,读取缓冲区的数据无需自己检查缓冲区是否有足够的字节。
如果没有足够的字节可用,这个readInt()方法的实现将会抛出一个Error,其将在基类中被捕获并处理。
捕获异常的逻辑如下:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
replayable.setCumulation(in);
try {
while (in.isReadable()) {
int oldReaderIndex = checkpoint = in.readerIndex();
int outSize = out.size();
S oldState = state;
int oldInputLength = in.readableBytes();
try {
decode(ctx, replayable, out);
} catch (Signal replay) {
//捕获Error
replay.expect(REPLAY);
//....................
}
}
}
//........................
}
使用ReplayingDecoder实现上述的需求:
@ChannelHandler.Sharable
class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
out.add(in.readInt()); //从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
}
}
ByteToMessageDecoder并没有考虑TCP拆包/粘包场景,更多关于Netty-TCP粘包/拆包解决之道。
更多解码器
- io.netty.handler.codec.LineBasedFrameDecoder—这个类在 Netty 内部也有使
用, 它使用了行尾控制字符(\n 或者\r\n)来解析消息数据;
- io.netty.handler.codec.http.HttpObjectDecoder—一个 HTTP 数据的解码器。
MessageToMessageDecoder
MessageToMessageDecoder用于从一种消息解码为另外一种消息(例如,POJO 到 POJO)。它实际上是Netty的一个二次解码器,它的职责将一个对象二次解码为其他对象。为什么称之为二次解码器?因为从SocketChannel读取的TCP数据报是ByteBuffer,它实际上是字节数组,首先要将它解码为一个Java对象,然后再通过MessageToMessageDecoder将它解码为另一个POJO对象。
在之前的需求之上,增加需求:将 Integer 转为 String。
@ChannelHandler.Sharable
class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
//第二个参数为msg,可见它已经有其他的解码器将ByteBuf转换为了Integer
@Override
protected void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
out.add(String.valueOf(msg));
}
}
TooLongFrameException类
由于 Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑, Netty 提供了TooLongFrameException 类, ,通常由解码器在帧太长时抛出。
为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个 TooLongFrameException(随后会被 ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如 HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。
@ChannelHandler.Sharable
class SafeByteToMessageDecoder extends ByteToMessageDecoder {
//设置一个最大字节数阈值
private static final int MAX_FRAME_SIZE = 1024;
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,
List<Object> out) throws Exception {
int readable = in.readableBytes();
//检查缓冲区中是否有超过 MAX_FRAME_SIZE个字节
if (readable > MAX_FRAME_SIZE) {
in.skipBytes(readable);
//跳过所有的可读字节,抛出 TooLongFrameException 并通知ChannelHandler
throw new TooLongFrameException("Frame too big!");
}
// do something
}
}
Eecoder(编码器)
encoder是用来把出站数据从一种格式转换到另外一种格式,因此它实现了ChanneOutboundHandler。
Netty 也提供了一组类来帮助你写encoder,当然这些类提供的是与decoder相反的方法,如下所示:
- 编码从消息到字节-MessageToByteEncoder
- 编码从消息到消息
MessageToByteEncoder
前面我们看到了如何使用ByteToMessageDecoder来将字节转换为消息。现在我们将使用MessageToByteEncoder来做逆向的事情。
这个类只有一个方法,而decoder却是有两个,原因就是decoder经常需要在Channel关闭时产生一个“最后的消息”。出于这个原因,提供了decodeLast。
与上例反向操作,将Integer编码成ByteBuf来发送到线上:
@ChannelHandler.Sharable
class IntegerToByteEncoder extends MessageToByteEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
//将Integer转换为ByteBuf
out.writeInt(msg);
}
}
MessageToMessageEncoder
使用MessageToMessageEncoder将出站数据将如何从一种消息编码为另一种。
将出站的消息由string转换为int:
@ChannelHandler.Sharable
class StringToIntegerEncoder extends MessageToMessageEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
out.add(Integer.parseInt(msg));
//接下来的encode 需要将 Integer转换为 字节码缓存ByteBuf
}
}
编解码器类
虽然我们一直把解码器和编码器作为不同的实体来讨论,但你有时可能会发现把入站和出站的数据和信息转换都放在同一个类中更实用。Netty的抽象编解码器类就是用于这个目的,他们把一些成对的解码器和编码器组合在一起,以此来提供对于字节和消息都相同的操作。(这些类实现了ChannelInboundHandler和ChannelOutboundHandler)。
ByteToMessageCodec<I>
它结合了ByteToMessageDecoder 以及它的逆向——MessageToByteEncoder。故它的api方法有:
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
decode(ctx, in, out);
}
MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN>
使用 MessageToMessageCodec,我们可以在一个单个的类中实现该转换的往返过程。
protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out) throws Exception;
protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out) throws Exception;
预置的ChannelHandler和编解码器
Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用, 这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。
SSL
为了支持SSL/TLS,Java提供了javax.net.ssl包,它的SSLContext和SSLEngine类使得实现解密和加密相当简单直接。Netty通过一个名为SslHandler的ChannelHandler实现利用了这个API,其中SslHandler在内部使用SSLEngine来完成实际的工作。下图展示了使用SslHandler的数据流。
在大多数情况下, SslHandler 将是 ChannelPipeline 中的第一个 ChannelHandler。这确保了只有在所有其他的 ChannelHandler 将它们的逻辑应用到数据之后,才会进行加密。
Netty 的 OpenSSL/SSLEngine 实现
Netty 还提供了使用 OpenSSL 工具包(www.openssl.org)的 SSLEngine 实现。这个 OpenSslEngine 类提供了比 JDK 提供的 SSLEngine 实现更好的性能。
如果OpenSSL 库可用,可以将 Netty 应用程序(客户端和服务器)配置为默认使用OpenSslEngine。
如果不可用, Netty 将会回退到 JDK 实现。有关配置 OpenSSL 支持的详细说明,参见 Netty 文档:
http://netty.io/wiki/forked-tomcat-native.html#wikih2-1。
注意,无论你使用 JDK 的 SSLEngine 还是使用 Netty 的 OpenSslEngine, SSL API 和数据流都
是一致的。
HTTP
HTTP是基于请求/响应模式的:客户端向服务器发送一个HTTP请求,然后服务器将会返回一个HTTP响应。
响应可能由多个数据部分组成,并且它总是以一个LastHttpContent部分作为结束。FullHttpRequest和FullHttpResponse消息是特殊的子类型,分别代表了完整的请求和响应。
if (client) {
//如果是客户端
ch.pipeline().addLast("decoder", new HttpResponseDecoder());
ch.pipeline().addLast("encoder", new HttpRequestEncoder());
} else {
ch.pipeline().addLast("decoder", new HttpRequestDecoder());
ch.pipeline().addLast("encoder", new HttpResponseEncoder());
}
//将多个HttpMessage组装为一个完整的FullHttpRequest或者FullHttpResponse
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
HTTP聚合
if (client) {
ch.pipeline().addLast("codec", new HttpClientCodec());
} else {
ch.pipeline().addLast("codec", new HttpServerCodec());
}
//将多个HttpMessage组装为一个完整的FullHttpRequest或者FullHttpResponse
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
HTTP压缩
当使用HTTP时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些CPU时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。
if (client) {
//如果是客户端,则添加HttpContentDecompressor 以处理来自服务器的压缩内容
ch.pipeline().addLast("codec", new HttpClientCodec());
ch.pipeline().addLast("decompressor",new HttpContentDecompressor());
} else {
//如果是服务器,则添加 HttpContentCompressor来压缩数据(如果客户端支持它)
ch.pipeline().addLast("codec", new HttpServerCodec());
ch.pipeline().addLast("compressor",new HttpContentCompressor());
}
HTTPS
ch.pipeline().addFirst("ssl", new SslHandler(engine));//ssl第一个handler
if (client) {
ch.pipeline().addLast("codec", new HttpClientCodec());
} else {
ch.pipeline().addLast("codec", new HttpServerCodec());
}
Websocket
ch.pipeline().addLast(
new HttpServerCodec(),
new HttpObjectAggregator(65536),
//如果被请求 HttpRequest的端点是"/websocket",则处理该升级握手
new WebSocketServerProtocolHandler("/websocket"),
new TextFrameHandler(),//TextFrameHandler 处理TextFrame
new BinaryFrameHandler(),//BinaryFrameHandler 处理BinaryFrame
new ContinuationFrameHandler());//ContinuationFrameHandler处理ContinuationWebSocketFrame
空闲的连接和超时
检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个 ChannelHandler 实现。
下例:当使用通常的发送心跳消息到远程节点的方法时,如果在 60 秒之内没有接收或者发送任何的数据,我们将如何得到通知;如果没有响应,则连接会被关闭
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//IdleStateHandler 将在被触发时发送一个 IdleStateEvent 事件
pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
}
}
class HeartbeatHandler extends ChannelHandlerAdapter {
private static final ByteBuf HEARTBEAT_SEQUENCE =
Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.UTF_8));
//实现 userEventTriggered()方法以发送心跳消息
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
//发送心跳消息,并在发送失败时关闭该连接
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
//不是 IdleStateEvent事件,所以将它传递给下一个 ChannelInboundHandler
super.userEventTriggered(ctx, evt);
}
}
}
解码分隔符和基于长度的协议
Netty-TCP粘包/拆包解决之道
写大型数据
因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入, 就有内存耗尽的风险。所以在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况, 这种情况会导致内存释放的延迟。
让我们考虑下将一个文件内容写出到网络的情况。
零拷贝
NIO的零拷贝特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在Netty的核心中,所以应用程序所有需要做的就是使用一个FileRegion接口的实现,其在Netty的API文档中的定义是:“通过支持零拷贝的文件传输的Channel来发送的文件区域。”
void zeroCopy(File file, SocketChannel channel) throws FileNotFoundException {
FileInputStream in = new FileInputStream(file);
//以该文件的完整长度创建一个新的 DefaultFileRegion
FileRegion region = new DefaultFileRegion(in.getChannel(), 0, file.length());
//发送该 DefaultFileRegion, 并注册一个ChannelFutureListener
channel.writeAndFlush(region).addListener(
new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future)
throws Exception {
if (!future.isSuccess()) {//处理失败
Throwable cause = future.cause();
// Do something
}
}
});
}
这个示例只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。在需要将数据从文件系统复制到用户内存中时,可以使用 ChunkedWriteHandler, 它支持异步写大型数据流,而又不会导致大量的内存消耗。
ChunkedWriteHandler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生java内存溢出错误
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
ch.pipeline().addLast("fileServerHandler", new FileServerHandler(url));
}
@ChannelHandler.Sharable
class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//......
RandomAccessFile = new RandomAccessFile(file, "r");
long fileLength = randomAccessFile.length();
ChannelFuture sendFileFuture = null;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
//......
});
}
}
序列化数据
Netty-编/解码技术