EmbeddedChannel類
Netty 提供了它所謂的 Embedded 傳輸,用於測試 ChannelHandler。這個傳輸是一種特殊的 Channel 實現— EmbeddedChannel— 的功能,這個實現提供了通過 ChannelPipeline 傳播事件的簡便方法。
這個想法是直截了當的:將入站數據或者出站數據寫入到 EmbeddedChannel 中,然後檢 查是否有任何東西到達了 ChannelPipeline 的尾端。以這種方式,你便可以確定消息是否已 經被編碼或者被解碼過了,以及是否觸發了任何的 ChannelHandler 動作。
入站數據由 ChannelInboundHandler 處理,代表從遠程節點讀取的數據。出站數據由 ChannelOutboundHandler 處理,代表將要寫到遠程節點的數據。根據你要測試的 ChannelHandler,你將使用*Inbound()或者*Outbound()
方法對,或者兼而有之。
下圖展示了使用 EmbeddedChannel 的方法,數據是如何流經 ChannelPipeline 的。 你可以使用 writeOutbound()方法將消息寫到 Channel 中,並通過 ChannelPipeline 沿 着出站的方向傳遞。隨後,你可以使用 readOutbound()方法來讀取已被處理過的消息,以確 定結果是否和預期一樣。類似地,對於入站數據,你需要使用 writeInbound()和 readInbound() 方法。
在每種情況下,消息都將會傳遞過 ChannelPipeline,並且被相關的 ChannelInbound- Handler 或者 ChannelOutboundHandler 處理。如果消息沒有被消費,那麼你可以使用 readInbound()或者 readOutbound()方法來在處理過了這些消息之後,酌情把它們從 Channel 中讀出來。
使用EmbeddedChannel測試ChannelHandler
測試入站消息:
下圖展示了一個簡單的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;
}
@Override
protected 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 {
@Test
public 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));
//將數據寫入EmbeddedChannel
System.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));//true
read = channel.readInbound();
System.out.println(buf.readSlice(3).equals(read));//true
read.release();
read = channel.readInbound();
System.out.println(buf.readSlice(3).equals(read));//true
read.release();
System.out.println(channel.readInbound() == null);//true
buf.release();
}
@Test
public 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)));//false
System.out.println(channel.writeInbound(input.readBytes(7)));//true
System.out.println(channel.finish());//true
ByteBuf read = channel.readInbound();
System.out.println(buf.readSlice(3) == read);//false
read.release();
read = channel.readInbound();
System.out.println(buf.readSlice(3) == read);//false
read.release();
read = channel.readInbound();
System.out.println(buf.readSlice(3) == read);//false
read.release();
System.out.println(channel.readInbound() == null);//true
buf.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> {
@Override
protected 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 {
@Test
public void testEncoded() {
ByteBuf buf = Unpooled.buffer();
for (int i = 1; i < 10; i++) {
buf.writeInt(i * -1);
}
//創建一個EmbeddedChanel,並安裝一個要測試的AbsIntegerEncoder
EmbeddedChannel 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;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int readableBytes = in.readableBytes();
if (readableBytes > maxFrameSize) {
//如果該幀太大,則丟棄它並拋出一個TooLongFrameException
in.clear();
throw new TooLongFrameException();
}
//否則,從ByteBuf中讀取一個新的幀
ByteBuf buf = in.readBytes(readableBytes);
//該幀添加到解碼消息的List中
out.add(buf);
}
}
測試FrameChunkDecoder:
public class FrameChunkDecoderTest {
@Test
public void testFramesDecoded() {
ByteBuf buf = Unpooled.buffer();
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
//創建一個EmbeddedChannel,並向其安裝一個幀大小爲3字節的FixedLengthFrameDecoder
EmbeddedChannel 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)));//true
System.out.println(channel.finish());
//讀取產生的消息,並且驗證值
ByteBuf read = channel.readInbound();
System.out.println(read.equals(buf.readSlice(2)));//true
read.release();
read = channel.readInbound();
System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//true
read.release();
buf.release();
}
}
這裏使用的try/catch塊是EmbeddedChannel的一個特 殊功能。如果其中一個write*方法產生了一個受檢查的Exception,那麼它將會被包裝在一個 RuntimeException中並拋出。這使得可以容易地測試出一個Exception是否在處理數據的 過程中已經被處理了。