6. ChannelHandler and ChannelPipeline
6.1 The ChannelHandler family
6.1.1 Channel的生命周期
ChannelUnregistered
已創建,但是還沒有被注冊到EventLoop上。
ChannelRegistered
已創建,并且已經注冊到EventLoop。
ChannelActive
連接上遠程主機。
ChannelActive
沒有連接到遠程主機。
Channel狀態的變化會觸發相應的事件。
6.1.2 ChannelHandler的生命周期
handlerAdd
添加handler
handlerRemove
刪除handler
exceptionCaught
發生異常
ChannelHandler有兩個重要的子接口:ChannelInboundHandler和ChannelOutboundHandler。
6.1.3 ChannelInboundHandler接口
接受到數據或者Channel的狀態發生改變會調用ChannelInboundHandler中的方法。注意,當ChannelInboundHandler中的channelRead()方法被overwrite,需要對ByteBuf實例持有的資源進行顯示釋放。
public class DiscardHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg);
}
}
可以使用SimpleChannelInboundHandler,它會自動釋放資源,無需人工干預:
@Sharable
public class SimpleDiscardHandler
extends SimpleChannelInboundHandler<Object> {
@Override
public void channelRead0(ChannelHandlerContext ctx,
Object msg) {
// No need to do anything special
}
}
6.1.4 ChannelOutboundHandler接口
它一個比較強大的功能是延遲執行。
CHANNELPROMISE VS. CHANNELFUTURE
CHANNELPROMISE是CHANNELFUTURE的子接口,CHANNELFUTURE是不可寫的,CHANNELPROMISE是可寫的(例如setSuccess(),setFailure()方法)
6.1.5 ChannelHandler adapters
6.1.6 資源管理
要注意ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write()要釋放相應的資源,否則會產生內存泄漏。netty使用引用計數法來管理內存資源。可以使用netty提供的ResourceLeakDetector來發現潛在的內存泄漏問題。
java -Dio.netty.leakDetectionLevel=ADVANCED
leakDetectionLevel可以為DISABLED、SIMPLE(默認)、ADVANCED和PARANOID。
6.2 ChannelPipeline接口
ChannelPipeline可以看成由ChannelHandler組成的鏈表,I/O事件會在ChannelPipeline上傳播。每個新Channel會綁定一個新ChannelPipeline,兩者是一對一關系。
事件傳播的時候,會判斷ChannelHandler的類型(implements Inbound還是OutBound的接口)和事件傳播的方向是否一致,不一致跳過。
6.2.1 ChannelPipeline修改
ChannelPipeline中ChannelHandler可以動態地被添加、刪除或者替換。
6.2.2 Firing events
會調用ChannelPipeline中下一個ChannelHandler里的方法。
代碼示例:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
public class HttpServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new MyHandler());
ch.pipeline().addLast(new MyHandler2());
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
class MyHandler extends SimpleChannelInboundHandler<String>{
@Override
protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("in MyHandler1 , messageReceived invoked");
for(int i = 0;i < 10 ; i++) {
ctx.fireChannelInactive();//調用fireChannelInactive 10次
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("in MyHandler1 , channelInactive invoked");
}
}
class MyHandler2 extends SimpleChannelInboundHandler<String>{
@Override
protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("in MyHandler2 ,messageReceived invoked");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("in MyHandler2 , channelInactive invoked");
}
}
輸出:
6.3 ChannelHandlerContext接口
ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之間的聯系,無論何時,添加一個ChannelHandler到ChannelPipeline就會創建一個ChannelHandlerContext。ChannelHandlerContext的主要功能是和所在ChannelPipeline的其他ChannelHandler交互。
ChannelHandlerContext有很多方法,大部分方法在Channel和ChannelPipeline里都出現過,但是這里有一個非常大的區別,調用Channel和ChannelPipeline里的方法,會在整個pipeline里傳播(從頭到尾),而ChannelHandlerContext里同名的方法,是從當前ChannelHandler開始傳播。
6.3.1 Using ChannelHandlerContext
6.3.2 ChannelHandler和ChannelHandlerContext的高級用法。
- ChannelHandlerContext的pipeline()方法可以獲取ChannelPipeline的引用,這樣我們可以通過這個引用操作ChannelHandler,實現動態協議。
- 可以把ChannelHandlerContext的引用緩存起來,在ChannelHandler方法外面用,甚至在一個不同的線程里使用。下面提供了一個示例。
- 可以將一個ChannelHandler實例可能會被添加到不同的ChannelPipeline里,但是需要使用@Sharable注解,此外還需注意的是,這個Sharable的ChannelHandler需要是線程安全的。
為什么需要@Sharable的ChannelHandler,一個需求就是通過這個@Sharable來統計多個Channel的數據。
6.4 異常處理
6.4.1 Inbound異常處理
由于exception默認會從觸發異常的ChannelHandler繼續向后流動,所以圖中的這種處理邏輯,我們一般放在最后ChannelPipeline的末尾。這樣就可以確保,無論是哪個ChannelHandler觸發異常,都能夠被捕獲并處理。如果不對異常做捕獲處理操作,netty會打印異常未被捕獲的日志。
6.4.2 outbound異常處理
進行outbound操作,要想知道結果(正常完成還是發生異常),需要這樣做:
每個outbound操作都會返回一個ChannelFuture。添加到ChannelFuture上的監聽器會收到成功或者錯誤通知。
ChannelOutboundHandler中的方法絕大多數都會ChannelPromise類型的參數。ChannelPromise也可以添加監聽來接受異步通知。ChannelPromise是可寫的,可以通過它的setSucess()方法或者setFailure(Throwable cause)立即發布通知。
如果ChannelOutboundHandler自己拋出異常,netty會通知添加到ChannelPromise上的監聽器。
7. EventLoop and threading model
7.1 Threading model overview
JDK早期版本多線程編程的方式是create
新線程再start
。JDK5推出了Executor API
,它的線程池技術通過緩存和重用大大提高了性能。
- 有任務(
Runnable實現
)的時候,從線程池里挑選出一個空閑線程,把任務submit
給它。 - 任務執行完畢了,線程變成空閑,回到線程池,等待下一次挑選使用。
線程池不能解決上下文切換開銷的問題,上下文的開銷在heavy load下會很大。
7.2 EventLoop接口
EventLoop
是一個用來處理事件的任務,基本思想如下圖所示:
EventLoop
接口的API分為兩類:concurrent和networking。
- concurrent
基于java.util.concurrent
包,提供thread executors - networking
io.netty.channel
繼承了EventLoop接口,提供了和Channel事件交互的能力。
7.2.1 Netty 4中I/O事件的處理
7.3.1 JDK 任務調度API
JDK5之前,任務調度只能用java.util.Timer
,Timer就是一個后臺線程,有很多限制:
- 如果執行多個定時任務,一個任務發生異常沒有捕獲,整個Timer線程會掛掉(其他所有任務都會down掉)
- 假如某個任務的執行時間過長,超過一些任務的間隔時間,會導致這些任務執行推遲。
JDK后續推出了java.util.concurrent
,其中定義的ScheduleExecutorService
克服了這些缺陷。
ScheduledExecutorService executor =Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);
//to do
executor.shutdown();
盡管ScheduledExecutorSevice
挺好用的,但是在負載大的時候有較大的性能耗費,netty進行了優化。
7.3.2 使用EventLoop進行任務調度
ScheduledExecutorService
也有一些限制,例如會創建額外創建一些線程來管理線程池,這在任務調度非常激烈的情況下,會成為性能的瓶頸。netty沒有直接使用ScheduledExecutorService
,使用了繼承于ScheduledExecutorService
,自己實現的EventLoop
。
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);
重復定時執行:
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println("Run every 60 seconds");
}
}, 60, 60, TimeUnit.Seconds);
7.4 實現細節
7.4.1 線程管理
netty線程模型的優越之處是在于它會確定當前執行線程的身份,再進行相應操作。如果當前執行線程被綁定到當前的Channel
和EventLoop
,會被直接執行,否則會被放到EventLoop
的隊列里,每個EventLoop
有自己單獨的隊列。
Never put a long-running task in the execution queue, because it will block any other task from executing on the same thread.” If you must make blocking calls or execute long-running tasks, we advise the use of a dedicated EventExecutor.
7.4.2 EventLoop/Thread分配
EventLoopGroup
包含了EventLoops
和Channels
,EventLoops
創建方式取決于使用哪種I/O.
異步I/O
異步I/O僅僅使用少量的EventLoops
,這些EventLoops
被很多的Channels
共享,這樣就可以用最少的線程接受很多的Channels
,而不是一個線程一個Channel
。
阻塞I/O
共同點:每個Channel
的I/O事件只會被一個線程處理。
8. Bootstrapping
bootstrapping an application is the process of configuring it to run
8.1 Bootstrap classes
Namely, a server devotes a parent channel to accepting connections from clients and
creating child channels for conversing with them, whereas a client will most likely
require only a single, non-parent channel for all network interactions. (As we’ll see, this
applies also to connectionless transports such as UDP , because they don’t require a
channel for each connection.)
server需要一個parent channel來接受客戶端連接,需要創建多個child channels來應答客戶端。
client只需要一個單獨的channel,不需要parent channel。
服務端處理使用ServerBootstrap
,客戶端使用Bootstrap
。
Why are the bootstrap classes Cloneable?
You’ll sometimes need to create multiple channels that have similar or identical settings. To support this pattern without requiring a new bootstrap instance to be created and configured for each channel, AbstractBootstrap has been marked Cloneable . Calling clone() on an already configured bootstrap will return another bootstrap instance that’s immediately usable. Note that this creates only a shallow copy of the bootstrap’s EventLoopGroup , so the latter will be shared among all of the cloned channels. This is acceptable, as the cloned channels are often short-lived, a typical case being a channel created to make an HTTP request.
8.2 Bootstrapping clients and connectionless protocols
Bootstrap
主要用來給客戶端和使用面向無連接的應用創建Channels
。
Bootstraping a client:
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channeRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Received data");
}
} );
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Connection established");
} else {
System.err.println("Connection attempt failed");
channelFuture.cause().printStackTrace();
}
}
} );
8.2.2 Channel和EventLoopGroup的兼容性
you can’t mix components having different
prefixes, such as NioEventLoopGroup and OioSocketChannel . The following listing
shows an attempt to do just that.
Channel
和EventLoopGroup
的前綴要一樣。否則會拋出IllegalStateException
8.3 Bootstraping servers
ServerBootstrap類
A ServerBootstrap creating a ServerChannel on bind() , and the ServerChannel managing a number of child Channels.
相比 Bootstrap
類,增加了childHandler()
,childAttr()
,childOption()
方法。ServerChannel
來創建許許多多的子Channel
,代表接受的連接。ServerBootstrap
提供了這些方法來簡化對子Channel
的配置。
NioEventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx,ByteBuf byteBuf) throw Exception {
System.out.println("Received data");
}
} );
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Server bound");
} else {
System.err.println("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
} );
8.4 Bootstrapping clients from a Channel
Suppose your server is processing a client request that requires it to act as a client to
a third system. This can happen when an application, such as a proxy server, has to
integrate with an organization’s existing systems, such as web services or databases. In
such cases you’ll need to bootstrap a client Channel from a ServerChannel
作為服務端接受連接,同時又作為客戶端,請求遠程服務器(類似于proxy),最容易想到的辦法是再創建一個客戶端的Bootstrap
,但是這樣需要另外一個EventLoop
來處理客戶端角色的Channel
,發生在服務端Channel
和客戶端Channel
之間數據交換引起的上文切換也會帶來額外的性能損耗。
最好的辦法是創建的客戶端Channel
和服務端Channel
,共享同一個EventLoop
:
ServerBootstrap bootstrap = new ServerBootstrap();
//Sets the EventLoopGroups that provide EventLoops for processing Channel events
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//Creates a Bootstrap to connect to remote host
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class).handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Received data");
}
});
//Uses the same EventLoop as the one assigned to the accepted channel
bootstrap.group(ctx.channel().eventLoop());
connectFuture = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf)
throws Exception {
if (connectFuture.isDone()) {
// do something with the data
//When the connection is complete performs some data operation (such as proxying)
}
}
});
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Server bound");
} else {
System.err.println("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
8.5 Adding multiple ChannelHandlers during a bootstrap
在bootstrap
的時候,如何添加多個ChannelHandler
?
netty提供了ChannelInboundHandlerAdapter
的特殊子類ChannelInitializer
:
public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter
ChannelInitializer
提供了initChannel()
可以輕松添加ChannelHandlers
到ChannelPipeline
。
protected abstract void initChannel(C ch) throws Exception;
一旦Channel
注冊到EventLoop
,我們實現的initChannel()
就會被調用。當initChannel()
返回的時候,ChannelInitializer
實例會把自己從ChannelPipeline
中刪除。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializerImpl());
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.sync();
對應ChannelInitializerImpl
的實現:
final class ChannelInitializerImpl extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}
8.6 Using Netty ChannelOptions and attributes
不需要我們手工配置每個Channel
,netty提供了option()
方法來把ChannelOptions
應用到bootstrap
,ChannelOptions
中的配置會自動地應用到所有Channel
Netty的Channel
和bootstrap
類,提供了AttributeMap
抽象集合和AttributeKey<T>
泛型類,用來insert和retrieve屬性值。使用這些工具,我們可以安全地把任意類型的數據和Channel
關聯起來。
Attribute
的一個使用場景是,服務端應用需要追蹤用戶和Channels
的關系。可以把用戶的ID作為一個屬性存到Channel
里。這樣就可以實現根據ID來路由消息和Channel
不活躍自動關閉等功能。
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID");
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup()).channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
Integer idValue = ctx.channel().attr(id).get();
// do something with the idValue
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf)
throws Exception {
System.out.println("Received data");
}
});
bootstrap.option(ChannelOption.SO_KEEPALIVE, true).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
bootstrap.attr(id, 123456);
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();
8.7 Bootstrapping DatagramChannels
之前的bootstrap
示例代碼都是基于TCP-based的SocketChannel
,bootstrap
也可以配置為無連接協議。
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new OioEventLoopGroup()).channel(OioDatagramChannel.class)
.handler(new SimpleChannelInboundHandler<DatagramPacket>() {
@Override
public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
// Do something with the packet
}
});
ChannelFuture future = bootstrap.bind(new InetSocketAddress(0));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Channel bound");
} else {
System.err.println("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
8.8 Shutdown
Alternatively, you can call Channel.close() explicitly on all active channels before calling EventLoopGroup.shutdownGracefully() . But in all cases, remember to shut down the EventLoopGroup itself.
EventLoopGroup.shutdownGracefully()
,它的返回值是一個future
,這也是一個異步操作。
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class);
...
Future<?> future = group.shutdownGracefully();
// block until the group has shutdown
future.syncUninterruptibly();
9 Unit testing
Netty提供了embedded transport
來測試ChannelHandlers
,embedded transport
是EmbeddedChannel
(一種特殊的Channel
實現) 的特色功能,可以簡單地實現在pipeline
中傳播事件。
我們可以寫入inbound
或者outbound
數據到EmbeddedChannel
,然后檢查是否有東西傳輸到ChannelPipeline
的末尾。我們還可以確定消息是否被編解碼,是否有ChannelHandler
被觸發。
Inbound data
會被ChannelInboundHandlers
處理,代表著從遠程主機讀取的數據。
outbound data
會被ChannelOutboundHandlers
處理,代表將要發送到遠程主機的數據。
相關API:
圖9.1展示了數據在EmbededChannel
的流動情況。我們可以:
使用
writeOutbound()
,寫入消息到Channel
,讓消息以outbound
方向在pipeline
中傳遞。后續,我們可以使用readOutbound()
讀取處理過后的數據,判斷結果是否與預期一致。使用
writeInbound()
,寫入消息到Channel
,讓消息以inbound
方向在pipeline
中傳遞。后續,我們可以使用readInbound()
讀取處理過后的數據,判斷結果是否與預期一致。
9.2 Testing ChannelHandlers with EmbeddedChannel
9.2.1 Testing inbound messages
圖9.2 展示了一個簡單的ByteToMessageDecoder
實現。如果有足夠的數據,這個Decoder會產生固定大小的frame。如果沒有足夠的數據,沒有達到這個固定的size值,它會等待接下來的數據,繼續判斷能否接著產生frame。
具體代碼實現如下:
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 buf = in.readBytes(frameLength);
out.add(buf);
}
}
}
那么如何進行單元測試呢,測試代碼如下:
public class FixedLengthFrameDecoderTest {
@Test
public void testFramesDecoded() {
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));
// write bytes
assertTrue(channel.writeInbound(input.retain()));
assertTrue(channel.finish());
// read messages
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
assertNull(channel.readInbound());
buf.release();
}
@Test
public void testFramesDecoded2() {
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));
assertFalse(channel.writeInbound(input.readBytes(2)));
assertTrue(channel.writeInbound(input.readBytes(7)));
assertTrue(channel.finish());
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
assertNull(channel.readInbound());
buf.release();
}
}
9.2.2 Testing outbound messages
我們需要測試一個編碼器:AbsIntegerEncoder
,它是Netty的MessageToMessageEncode
的一個實現,功能是將整數取絕對值。
我們的流程如下:
EmbeddedChannel
會將一個四字節負數按照outbound方向寫入Channel
。編碼器會從到來的
ByteBuf
讀取每個負數,調用Math.abs()
獲得絕對值。-
編碼器將絕對值寫入到
ChannelHandlerPipe
。
編碼器代碼實現:
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() >= 4) {
int value = Math.abs(in.readInt());
out.add(value);
}
}
}
怎么測試?請看下文:
public class AbsIntegerEncoderTest {
@Test
public void testEncoded() {
ByteBuf buf = Unpooled.buffer();
for (int i = 1; i < 10; i++) {
buf.writeInt(i * -1);
}
EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());
assertTrue(channel.writeOutbound(buf));
assertTrue(channel.finish());
// read bytes
for (int i = 1; i < 10; i++) {
assertEquals(i, channel.readOutbound());
}
assertNull(channel.readOutbound());
}
}
9.3 Testing exception handling
為了測試異常處理,我們有如下的示例。
為防止資源耗盡,當我們讀取到的數據多于某個數值,我們會拋出一個TooLongFrameException
在圖9.4中,最大frame的大小為3字節,當一個frame的字節數大于3,它會被忽略,并且會拋出
TooLongFrameException
,其他的pipeline
里的其他ChannelHandlers
要么覆寫exceptionCaught()
進行捕獲處理,要么會忽略這個異常。
解碼器代碼:
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) {
// discard the bytes
in.clear();
throw new TooLongFrameException();
}
ByteBuf buf = in.readBytes(readableBytes);
out.add(buf);
}
}
如何測試,請看:
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 channel = new EmbeddedChannel(new FrameChunkDecoder(3));
assertTrue(channel.writeInbound(input.readBytes(2)));
try {
channel.writeInbound(input.readBytes(4));
Assert.fail();
} catch (TooLongFrameException e) {
// expected exception
}
assertTrue(channel.writeInbound(input.readBytes(3)));
assertTrue(channel.finish());
// Read frames
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(2), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.skipBytes(4).readSlice(3), read);
read.release();
buf.release();
}
}
10.The codec framework
encoder
,將outbound
消息轉換成易于傳輸的方式(大部分是字節流)。
decoder
,將inbound
網絡字節流轉回成應用程序消息格式。
10.2 Decoders
兩種場景需要使用到Decoders
:
- 將字節流解碼成消息--
ByteToMessageDecoder
和ReplayingDecoder
- 將一種消息類型解碼成另一種類型--
MessageToMessageDecoder
10.2.1 ByteToMessageDecoder抽象類
功能: 將字節流解碼成消息或者另一種字節流。
使用示例ToIntegerDecoder
:
每次從ByteBuf
讀取四個字節,解碼成int
,添加到List
里。當沒有更多的數據添加到List
,List
里的內容會傳遞到下一個ChannelInboundHandler
。
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}
編解碼框架里,消息處理完了,會自動調用ReferenceCountUtil.release(message)
,資源會自動釋放。
Reference counting in codecs
As we mentioned in chapters 5 and 6, reference counting requires special attention. In the case of encoders and decoders, the procedure is quite simple: once a mes- sage has been encoded or decoded, it will automatically be released by a call toReferenceCountUtil.release(message)
. If you need to keep a reference for later use you can callReferenceCountUtil.retain(message)
. This increments the reference count, preventing the message from being released.
10.2.2 ReplayingDecoder抽象類
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
ReplayingDecoder
繼承于ByteToMessageDecoder
,特點是我們不再需要調用readableBytes()
,省了判斷數據是否足夠的邏輯。
注意:
不是所有的
ByteBuf
的操作都被支持。如果不支持會拋出UnsupportedOperationException
異常。ReplayingDecoder
會比ByteToMessageDecoder
稍慢。
ToIntegerDecoder2
:
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
out.add(in.readInt());
}
}
更多的解碼工具可以在io.netty.handler.codec
下找到。
io.netty.handler.codec.LineBasedFrameDecoder
,通過換行符(\n
或者\r\n
)來解析消息。io.netty.handler.codec.http.HttpObjectDecoder
,解析HTTP數據。
10.2.3 MessageToMessageDecoder抽象類
消息格式互相轉換,如把一種類型的POJO轉換成另外一種。
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
API差不多
示例:IntegerToStringDecoder
public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
@Override
public void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
out.add(String.valueOf(msg));
}
}
一個更貼切詳細的例子是io.netty.handler.codec.http.HttpObjectAggregator
用 TooLongFrameException
防止資源耗盡:
public 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();
if (readable > MAX_FRAME_SIZE) {
in.skipBytes(readable);
throw new TooLongFrameException("Frame too big!");
}
// do something
}
}
10.3 Encoders
與解碼器類似,Encoders
分為兩種:
- 將消息編碼成字節流。
- 將一種消息編碼成另一種格式的消息。
10.3.1 MessageToByteEncoder抽象類
示例ShortToByteEncoder
:
public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
@Override
public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out) throws Exception {
out.writeShort(msg);
}
}
更具體的應用實踐可以參見io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder
10.4 編解碼抽象類
既能encode
,又能decode
,二合一。
10.4.1 ByteToMessageCodec抽象類
Any request/response protocol could be a good candidate for using the
ByteToMessageCodec
. For example, in an SMTP implementation, the codec would read incoming bytes and decode them to a custom message type, saySmtpRequest
. On the receiving side, when a response is created, anSmtpResponse
will be produced, which will be encoded back to bytes for transmission.
10.4.2 MessageToMessageCodec抽象類
public abstract class MessageToMessageCodec<INBOUND_IN,OUTBOUND_IN>
public class WebSocketConvertHandler extends MessageToMessageCodec<WebSocketFrame, WebSocketConvertHandler.MyWebSocketFrame> {
@Override
protected void encode(ChannelHandlerContext ctx,
WebSocketConvertHandler.MyWebSocketFrame msg, List<Object> out)
throws Exception {
ByteBuf payload = msg.getData().duplicate().retain();
switch (msg.getType()) {
case BINARY:
out.add(new BinaryWebSocketFrame(payload));
break;
case TEXT:
out.add(new TextWebSocketFrame(payload));
break;
case CLOSE:
out.add(new CloseWebSocketFrame(true, 0, payload));
break;
case CONTINUATION:
out.add(new ContinuationWebSocketFrame(payload));
break;
case PONG:
out.add(new PongWebSocketFrame(payload));
break;
case PING:
out.add(new PingWebSocketFrame(payload));
break;
default:
throw new IllegalStateException("Unsupported websocket msg " + msg);
}
}
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg,
List<Object> out) throws Exception {
ByteBuf payload = msg.getData().duplicate().retain();
if (msg instanceof BinaryWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.BINARY,
payload));
} else if (msg instanceof CloseWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.CLOSE,
payload));
} else if (msg instanceof PingWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PING,
payload));
} else if (msg instanceof PongWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PONG,
payload));
} else if (msg instanceof TextWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.TEXT,
payload));
} else if (msg instanceof ContinuationWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.CONTINUATION, payload));
} else {
throw new IllegalStateException("Unsupported websocket msg " + msg);
}
}
public static final class MyWebSocketFrame {
private final FrameType type;
private final ByteBuf data;
public WebSocketFrame(FrameType type, ByteBuf data) {
this.type = type;
this.data = data;
}
public FrameType getType() {
return type;
}
public ByteBuf getData() {
return data;
}
public enum FrameType {BINARY,
CLOSE,
PING,
PONG,
TEXT,
CONTINUATION;
}
}
}
10.4.3 CombinedChannelDuplexHandler類
將編碼器解碼器放在一塊影響代碼的重用性。CombinedChannelDuplexHandler
可以解決這個問題。我們可以使用它而不直接使用codec抽象類。
方法簽名:
public class CombinedChannelDuplexHandler <I extends ChannelInboundHandler, O extends ChannelOutboundHandler>
下面是一個使用范例:
解碼器例子ByteToCharDecoder
功能是一次讀取2個字節,解碼成char
寫到List
里
public class ByteToCharDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
while (in.readableBytes() >= 2) {
out.add(in.readChar());
}
}
}
編碼器例子CharToByteEncoder
public class CharToByteEncoder extends MessageToByteEncoder<Character> {
@Override
public void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out)
throws Exception {
out.writeChar(msg);
}
}
是時候combine
了:
public class CombinedByteCharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
public CombinedByteCharCodec() {
super(new ByteToCharDecoder(), new CharToByteEncoder());
}
}