《netty in action》讀書筆記 PART1

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有兩個重要的子接口:ChannelInboundHandlerChannelOutboundHandler

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,兩者是一對一關系。


pipeline中的事件傳播

事件傳播的時候,會判斷ChannelHandler的類型(implements Inbound還是OutBound的接口)和事件傳播的方向是否一致,不一致跳過。

6.2.1 ChannelPipeline修改

ChannelPipeline中ChannelHandler可以動態地被添加、刪除或者替換。


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的高級用法。

  1. ChannelHandlerContext的pipeline()方法可以獲取ChannelPipeline的引用,這樣我們可以通過這個引用操作ChannelHandler,實現動態協議
  2. 可以把ChannelHandlerContext的引用緩存起來,在ChannelHandler方法外面用,甚至在一個不同的線程里使用。下面提供了一個示例。
引用緩存實例
  1. 可以將一個ChannelHandler實例可能會被添加到不同的ChannelPipeline里,但是需要使用@Sharable注解,此外還需注意的是,這個Sharable的ChannelHandler需要是線程安全的。

為什么需要@Sharable的ChannelHandler,一個需求就是通過這個@Sharable來統計多個Channel的數據。

6.4 異常處理

6.4.1 Inbound異常處理

Inbound異常處理

由于exception默認會從觸發異常的ChannelHandler繼續向后流動,所以圖中的這種處理邏輯,我們一般放在最后ChannelPipeline的末尾。這樣就可以確保,無論是哪個ChannelHandler觸發異常,都能夠被捕獲并處理。如果不對異常做捕獲處理操作,netty會打印異常未被捕獲的日志。

6.4.2 outbound異常處理

進行outbound操作,要想知道結果(正常完成還是發生異常),需要這樣做:

  1. 每個outbound操作都會返回一個ChannelFuture。添加到ChannelFuture上的監聽器會收到成功或者錯誤通知。

  2. 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,它的線程池技術通過緩存和重用大大提高了性能。

  1. 有任務(Runnable實現)的時候,從線程池里挑選出一個空閑線程,把任務submit給它。
  2. 任務執行完畢了,線程變成空閑,回到線程池,等待下一次挑選使用。
線程池技術

線程池不能解決上下文切換開銷的問題,上下文的開銷在heavy load下會很大。

7.2 EventLoop接口

EventLoop是一個用來處理事件的任務,基本思想如下圖所示:

image.png

EventLoop接口的API分為兩類:concurrent和networking。

  1. concurrent
    基于java.util.concurrent包,提供thread executors
  2. networking
    io.netty.channel繼承了EventLoop接口,提供了和Channel事件交互的能力。

7.2.1 Netty 4中I/O事件的處理

7.3.1 JDK 任務調度API

JDK5之前,任務調度只能用java.util.Timer,Timer就是一個后臺線程,有很多限制:

  1. 如果執行多個定時任務,一個任務發生異常沒有捕獲,整個Timer線程會掛掉(其他所有任務都會down掉)
  2. 假如某個任務的執行時間過長,超過一些任務的間隔時間,會導致這些任務執行推遲。

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線程模型的優越之處是在于它會確定當前執行線程的身份,再進行相應操作。如果當前執行線程被綁定到當前的ChannelEventLoop,會被直接執行,否則會被放到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包含了EventLoopsChannelsEventLoops創建方式取決于使用哪種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.

ChannelEventLoopGroup的前綴要一樣。否則會拋出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()可以輕松添加ChannelHandlersChannelPipeline

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應用到bootstrapChannelOptions中的配置會自動地應用到所有Channel

Netty的Channelbootstrap類,提供了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的SocketChannelbootstrap也可以配置為無連接協議。

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 transportEmbeddedChannel (一種特殊的Channel實現) 的特色功能,可以簡單地實現在pipeline中傳播事件。

我們可以寫入inbound或者outbound數據到EmbeddedChannel,然后檢查是否有東西傳輸到ChannelPipeline的末尾。我們還可以確定消息是否被編解碼,是否有ChannelHandler被觸發。

Inbound data會被ChannelInboundHandlers處理,代表著從遠程主機讀取的數據。

outbound data會被ChannelOutboundHandlers處理,代表將要發送到遠程主機的數據。

相關API:

圖9.1展示了數據在EmbededChannel的流動情況。我們可以:

  1. 使用writeOutbound(),寫入消息到Channel,讓消息以outbound方向在pipeline中傳遞。后續,我們可以使用readOutbound()讀取處理過后的數據,判斷結果是否與預期一致。

  2. 使用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的一個實現,功能是將整數取絕對值。

我們的流程如下:

  1. EmbeddedChannel會將一個四字節負數按照outbound方向寫入Channel

  2. 編碼器會從到來的ByteBuf讀取每個負數,調用Math.abs()獲得絕對值。

  3. 編碼器將絕對值寫入到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:

  1. 將字節流解碼成消息--ByteToMessageDecoderReplayingDecoder
  2. 將一種消息類型解碼成另一種類型--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 to ReferenceCountUtil.release(message) . If you need to keep a reference for later use you can call ReferenceCountUtil.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(),省了判斷數據是否足夠的邏輯。

注意:

  1. 不是所有的ByteBuf的操作都被支持。如果不支持會拋出UnsupportedOperationException異常。

  2. 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下找到。

  1. io.netty.handler.codec.LineBasedFrameDecoder,通過換行符(\n或者\r\n)來解析消息。

  2. 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分為兩種:

  1. 將消息編碼成字節流。
  2. 將一種消息編碼成另一種格式的消息。

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, say SmtpRequest . On the receiving side, when a response is created, an SmtpResponse 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());
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容