netty(6)
netty(6)–单元测试
EmbeddedChannel, Netty 专门为改进针对ChannelHandler 的单元测试而提供的。
Netty 提供了它所谓的 Embedded 传输,用于测试 ChannelHandler。这个传输是一种特殊的 Channel 实现— EmbeddedChannel— 的功能,这个实现提供了通过 ChannelPipeline 传播事件的简便方法。
这个想法是直截了当的:将入站数据或者出站数据写入到 EmbeddedChannel 中,然后检 查是否有任何东西到达了 ChannelPipeline 的尾端。以这种方式,你便可以确定消息是否已 经被编码或者被解码过了,以及是否触发了任何的 ChannelHandler 动作。
入站数据由 ChannelInboundHandler 处理,代表从远程节点读取的数据。出站数据由ChannelOutboundHandler 处理,代表将要写到远程节点的数据。根据你要测试的ChannelHandler,你将使用**Inbound()或者Outbound()**方法对,或者兼而有之。
我们可以使用 writeOutbound()方法将消息写到 Channel 中,并通过 ChannelPipeline 沿 着出站的方向传递。也可以使用 readOutbound()方法来读取已被处理过的消息,以确 定结果是否和预期一样。类似地,对于入站数据,你需要使用 writeInbound()和 readInbound() 方法。
测试入站消息
下图展示了一个简单的ByteToMessageDecoder实现。给定足够的数据,这个实现将 产生固定大小的帧。如果没有足够的数据可供读取,它将等待下一个数据块的到来,并将再次检 查是否能够产生一个新的帧。
这个特定的解码器将产生固定为 3 字节大小的帧。因此,它可能会需要多个事件来提供足够的字节数以产生一个帧。最终,每个帧都会被传递给 ChannelPipeline 中的下一个ChannelHandler。
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {private final int frameLength;public FixedLengthFrameDecoder(int frameLength) {if (frameLength <= 0) {throw new IllegalArgumentException("frameLength must be a positive integer:" + frameLength);}this.frameLength = frameLength;}@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {//检查是否有足够的字节可以被读取,以生成下一个帧while (in.readableBytes() >= frameLength) {//从ByteBuf中读取一个新帧ByteBuf buf = in.readBytes(frameLength);//将该帧添加到已被解码的消息列表中out.add(buf);}}
}
测试FixedLengthFrameDecoder
public class FixedLengthFrameDecoderTest {@Testpublic void testFrameDecoded() {//创建一个ByteBuf,并存储9字节ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));//将数据写入EmbeddedChannelSystem.out.println(channel.writeInbound(input.retain()));//true//标记Channel为已完成状态System.out.println(channel.finish());//true//读取所生成的消息,并且验证是否有3帧,其中每帧都为3字节ByteBuf read = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread.release();read = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread.release();System.out.println(channel.readInbound() == null);//truebuf.release();}@Testpublic void testFramesDescode2() {ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));//返回false,因为没有一个完整的可供读取的帧System.out.println(channel.writeInbound(input.readBytes(2)));//falseSystem.out.println(channel.writeInbound(input.readBytes(7)));//trueSystem.out.println(channel.finish());//trueByteBuf read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();System.out.println(channel.readInbound() == null);//truebuf.release();}}
该 testFramesDecoded()方法验证了:一个包含 9 个可读字节的 ByteBuf 被解码为 3 个 ByteBuf,每个都包含了 3 字节。需要注意的是,仅通过一次对 writeInbound()方法的调 用,ByteBuf 是如何被填充了 9 个可读字节的。在此之后,通过执行 finish()方法,将 EmbeddedChannel 标记为了已完成状态。最后,通过调用 readInbound()方法,从 Embedded- Channel 中正好读取了 3 个帧和一个 null。
testFramesDecoded2()方法也是类似的,只有一处不同:入站 ByteBuf 是通过两个步 骤写入的。当 writeInbound(input.readBytes(2))被调用时,返回了 false。为什么呢? 正如同上表中所描述的,如果对 readInbound()的后续调用将会返回数据,那么 write- Inbound()方法将会返回 true。但是只有当有 3 个或者更多的字节可供读取时,FixedLengthFrameDecoder 才会产生输出。该测试剩下的部分和 testFramesDecoded()是相同的。
测试出站消息
测试出站消息的处理过程和刚才所看到的类似。在下面的例子中,我们将会展示如何使用 EmbeddedChannel 来测试一个编码器形式的 ChannelOutboundHandler,编码器是一种 将一种消息格式转换为另一种的组件。
AbsIntegerEncoder,它是 Netty 的 MessageToMessageEncoder 的一个特殊化的实现,用于将负值整数转换为绝对值。
该示例将会按照下列方式工作:
- 持有 AbsIntegerEncoder 的 EmbeddedChannel 将会以 4 字节的负整数的形式写出站数据;
- 编码器将从传入的 ByteBuf 中读取每个负整数,并将会调用 Math.abs()方法来获取其绝对值;
- 编码器将会把每个负整数的绝对值写到 ChannelPipeline 中。
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {@Overrideprotected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {while (msg.readableBytes() >= 4) {//从输入的ByteBuf中读取下一个整数,并且计算其绝对值int value = Math.abs(msg.readInt());//将该整数写入到编码消息的List中out.add(value);}}
}
测试AbsIntegerEncoder
public class AbsIntegerEncoderTest {@Testpublic void testEncoded() {ByteBuf buf = Unpooled.buffer();for (int i = 1; i < 10; i++) {buf.writeInt(i * -1);}//创建一个EmbeddedChanel,并安装一个要测试的AbsIntegerEncoderEmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());//写入ByteBuf,调用readOutbound()方法将会产生数据System.out.println(channel.writeOutbound(buf));System.out.println(channel.finish());channel.readOutbound();for (int i = 1; i < 10; i++) {int temp = channel.readOutbound();System.out.println(temp);}System.out.println(channel.readOutbound() == null);}
}
下面是代码中执行的步骤。
- 将 4 字节的负整数写到一个新的 ByteBuf 中。
- 创建一个 EmbeddedChannel,并为它分配一个 AbsIntegerEncoder。
- 调用 EmbeddedChannel 上的 writeOutbound()方法来写入该 ByteBuf。
- 标记该 Channel 为已完成状态。
- 从 EmbeddedChannel 的出站端读取所有的整数,并验证是否只产生了绝对值。
测试异常处理
应用程序通常需要执行比转换数据更加复杂的任务。例如,你可能需要处理格式不正确的输 入或者过量的数据。在下一个示例中,如果所读取的字节数超出了某个特定的限制,我们将会抛 出一个 TooLongFrameException。这是一种经常用来防范资源被耗尽的方法。
在下图中,最大的帧大小已经被设置为 3 字节。如果一个帧的大小超出了该限制,那么程序将 会丢弃它的字节,并抛出一个 TooLongFrameException。位于 ChannelPipeline 中的其他 ChannelHandler 可以选择在 exceptionCaught()方法中处理该异常或者忽略它:
//扩展ByteToMessageDecoder以将入站字节码为消息
public class FrameChunkDecoder extends ByteToMessageDecoder {private final int maxFrameSize;public FrameChunkDecoder(int maxFrameSize) {this.maxFrameSize = maxFrameSize;}@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {int readableBytes = in.readableBytes();if (readableBytes > maxFrameSize) {//如果该帧太大,则丢弃它并抛出一个TooLongFrameExceptionin.clear();throw new TooLongFrameException();}//否则,从ByteBuf中读取一个新的帧ByteBuf buf = in.readBytes(readableBytes);//该帧添加到解码消息的List中out.add(buf);}
}
测试FrameChunkDecoder
public class FrameChunkDecoderTest {@Testpublic void testFramesDecoded() {ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();//创建一个EmbeddedChannel,并向其安装一个帧大小为3字节的FixedLengthFrameDecoderEmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));System.out.println(channel.writeInbound(input.readBytes(2)));try {//写入一个4字节大小的帧,并捕获预期的异常channel.writeInbound(input.readBytes(4));} catch (TooLongFrameException e) {e.printStackTrace();}//写入剩余的2字节,会产生一个有效帧System.out.println(channel.writeInbound(input.readBytes(3)));//trueSystem.out.println(channel.finish());//读取产生的消息,并且验证值ByteBuf read = channel.readInbound();System.out.println(read.equals(buf.readSlice(2)));//trueread.release();read = channel.readInbound();System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//trueread.release();buf.release();}
}
intln(read.equals(buf.readSlice(2)));//true
read.release();
read = channel.readInbound();System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//trueread.release();buf.release();}
}
这里使用的try/catch块是EmbeddedChannel的一个特 殊功能。如果其中一个write*方法产生了一个受检查的Exception,那么它将会被包装在一个 RuntimeException中并抛出。这使得可以容易地测试出一个Exception是否在处理数据的 过程中已经被处理了。
netty(6)
netty(6)–单元测试
EmbeddedChannel, Netty 专门为改进针对ChannelHandler 的单元测试而提供的。
Netty 提供了它所谓的 Embedded 传输,用于测试 ChannelHandler。这个传输是一种特殊的 Channel 实现— EmbeddedChannel— 的功能,这个实现提供了通过 ChannelPipeline 传播事件的简便方法。
这个想法是直截了当的:将入站数据或者出站数据写入到 EmbeddedChannel 中,然后检 查是否有任何东西到达了 ChannelPipeline 的尾端。以这种方式,你便可以确定消息是否已 经被编码或者被解码过了,以及是否触发了任何的 ChannelHandler 动作。
入站数据由 ChannelInboundHandler 处理,代表从远程节点读取的数据。出站数据由ChannelOutboundHandler 处理,代表将要写到远程节点的数据。根据你要测试的ChannelHandler,你将使用**Inbound()或者Outbound()**方法对,或者兼而有之。
我们可以使用 writeOutbound()方法将消息写到 Channel 中,并通过 ChannelPipeline 沿 着出站的方向传递。也可以使用 readOutbound()方法来读取已被处理过的消息,以确 定结果是否和预期一样。类似地,对于入站数据,你需要使用 writeInbound()和 readInbound() 方法。
测试入站消息
下图展示了一个简单的ByteToMessageDecoder实现。给定足够的数据,这个实现将 产生固定大小的帧。如果没有足够的数据可供读取,它将等待下一个数据块的到来,并将再次检 查是否能够产生一个新的帧。
这个特定的解码器将产生固定为 3 字节大小的帧。因此,它可能会需要多个事件来提供足够的字节数以产生一个帧。最终,每个帧都会被传递给 ChannelPipeline 中的下一个ChannelHandler。
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {private final int frameLength;public FixedLengthFrameDecoder(int frameLength) {if (frameLength <= 0) {throw new IllegalArgumentException("frameLength must be a positive integer:" + frameLength);}this.frameLength = frameLength;}@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {//检查是否有足够的字节可以被读取,以生成下一个帧while (in.readableBytes() >= frameLength) {//从ByteBuf中读取一个新帧ByteBuf buf = in.readBytes(frameLength);//将该帧添加到已被解码的消息列表中out.add(buf);}}
}
测试FixedLengthFrameDecoder
public class FixedLengthFrameDecoderTest {@Testpublic void testFrameDecoded() {//创建一个ByteBuf,并存储9字节ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));//将数据写入EmbeddedChannelSystem.out.println(channel.writeInbound(input.retain()));//true//标记Channel为已完成状态System.out.println(channel.finish());//true//读取所生成的消息,并且验证是否有3帧,其中每帧都为3字节ByteBuf read = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread.release();read = channel.readInbound();System.out.println(buf.readSlice(3).equals(read));//trueread.release();System.out.println(channel.readInbound() == null);//truebuf.release();}@Testpublic void testFramesDescode2() {ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));//返回false,因为没有一个完整的可供读取的帧System.out.println(channel.writeInbound(input.readBytes(2)));//falseSystem.out.println(channel.writeInbound(input.readBytes(7)));//trueSystem.out.println(channel.finish());//trueByteBuf read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();read = channel.readInbound();System.out.println(buf.readSlice(3) == read);//falseread.release();System.out.println(channel.readInbound() == null);//truebuf.release();}}
该 testFramesDecoded()方法验证了:一个包含 9 个可读字节的 ByteBuf 被解码为 3 个 ByteBuf,每个都包含了 3 字节。需要注意的是,仅通过一次对 writeInbound()方法的调 用,ByteBuf 是如何被填充了 9 个可读字节的。在此之后,通过执行 finish()方法,将 EmbeddedChannel 标记为了已完成状态。最后,通过调用 readInbound()方法,从 Embedded- Channel 中正好读取了 3 个帧和一个 null。
testFramesDecoded2()方法也是类似的,只有一处不同:入站 ByteBuf 是通过两个步 骤写入的。当 writeInbound(input.readBytes(2))被调用时,返回了 false。为什么呢? 正如同上表中所描述的,如果对 readInbound()的后续调用将会返回数据,那么 write- Inbound()方法将会返回 true。但是只有当有 3 个或者更多的字节可供读取时,FixedLengthFrameDecoder 才会产生输出。该测试剩下的部分和 testFramesDecoded()是相同的。
测试出站消息
测试出站消息的处理过程和刚才所看到的类似。在下面的例子中,我们将会展示如何使用 EmbeddedChannel 来测试一个编码器形式的 ChannelOutboundHandler,编码器是一种 将一种消息格式转换为另一种的组件。
AbsIntegerEncoder,它是 Netty 的 MessageToMessageEncoder 的一个特殊化的实现,用于将负值整数转换为绝对值。
该示例将会按照下列方式工作:
- 持有 AbsIntegerEncoder 的 EmbeddedChannel 将会以 4 字节的负整数的形式写出站数据;
- 编码器将从传入的 ByteBuf 中读取每个负整数,并将会调用 Math.abs()方法来获取其绝对值;
- 编码器将会把每个负整数的绝对值写到 ChannelPipeline 中。
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {@Overrideprotected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {while (msg.readableBytes() >= 4) {//从输入的ByteBuf中读取下一个整数,并且计算其绝对值int value = Math.abs(msg.readInt());//将该整数写入到编码消息的List中out.add(value);}}
}
测试AbsIntegerEncoder
public class AbsIntegerEncoderTest {@Testpublic void testEncoded() {ByteBuf buf = Unpooled.buffer();for (int i = 1; i < 10; i++) {buf.writeInt(i * -1);}//创建一个EmbeddedChanel,并安装一个要测试的AbsIntegerEncoderEmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());//写入ByteBuf,调用readOutbound()方法将会产生数据System.out.println(channel.writeOutbound(buf));System.out.println(channel.finish());channel.readOutbound();for (int i = 1; i < 10; i++) {int temp = channel.readOutbound();System.out.println(temp);}System.out.println(channel.readOutbound() == null);}
}
下面是代码中执行的步骤。
- 将 4 字节的负整数写到一个新的 ByteBuf 中。
- 创建一个 EmbeddedChannel,并为它分配一个 AbsIntegerEncoder。
- 调用 EmbeddedChannel 上的 writeOutbound()方法来写入该 ByteBuf。
- 标记该 Channel 为已完成状态。
- 从 EmbeddedChannel 的出站端读取所有的整数,并验证是否只产生了绝对值。
测试异常处理
应用程序通常需要执行比转换数据更加复杂的任务。例如,你可能需要处理格式不正确的输 入或者过量的数据。在下一个示例中,如果所读取的字节数超出了某个特定的限制,我们将会抛 出一个 TooLongFrameException。这是一种经常用来防范资源被耗尽的方法。
在下图中,最大的帧大小已经被设置为 3 字节。如果一个帧的大小超出了该限制,那么程序将 会丢弃它的字节,并抛出一个 TooLongFrameException。位于 ChannelPipeline 中的其他 ChannelHandler 可以选择在 exceptionCaught()方法中处理该异常或者忽略它:
//扩展ByteToMessageDecoder以将入站字节码为消息
public class FrameChunkDecoder extends ByteToMessageDecoder {private final int maxFrameSize;public FrameChunkDecoder(int maxFrameSize) {this.maxFrameSize = maxFrameSize;}@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {int readableBytes = in.readableBytes();if (readableBytes > maxFrameSize) {//如果该帧太大,则丢弃它并抛出一个TooLongFrameExceptionin.clear();throw new TooLongFrameException();}//否则,从ByteBuf中读取一个新的帧ByteBuf buf = in.readBytes(readableBytes);//该帧添加到解码消息的List中out.add(buf);}
}
测试FrameChunkDecoder
public class FrameChunkDecoderTest {@Testpublic void testFramesDecoded() {ByteBuf buf = Unpooled.buffer();for (int i = 0; i < 9; i++) {buf.writeByte(i);}ByteBuf input = buf.duplicate();//创建一个EmbeddedChannel,并向其安装一个帧大小为3字节的FixedLengthFrameDecoderEmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));System.out.println(channel.writeInbound(input.readBytes(2)));try {//写入一个4字节大小的帧,并捕获预期的异常channel.writeInbound(input.readBytes(4));} catch (TooLongFrameException e) {e.printStackTrace();}//写入剩余的2字节,会产生一个有效帧System.out.println(channel.writeInbound(input.readBytes(3)));//trueSystem.out.println(channel.finish());//读取产生的消息,并且验证值ByteBuf read = channel.readInbound();System.out.println(read.equals(buf.readSlice(2)));//trueread.release();read = channel.readInbound();System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//trueread.release();buf.release();}
}
intln(read.equals(buf.readSlice(2)));//true
read.release();
read = channel.readInbound();System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//trueread.release();buf.release();}
}
这里使用的try/catch块是EmbeddedChannel的一个特 殊功能。如果其中一个write*方法产生了一个受检查的Exception,那么它将会被包装在一个 RuntimeException中并抛出。这使得可以容易地测试出一个Exception是否在处理数据的 过程中已经被处理了。