Netty——預置的ChannelHandler和編解碼器(二)

空閒的連接和超時

檢測空閒連接以及超時對於及時釋放資源來說是至關重要的。由於這是一項常見的任務,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));
        // 將一個 HeartbeatHandler 添加到ChannelPipeline 中
        pipeline.addLast(new HearbeatHandler());
    }

    //實現userEventTriggred()方法以發送心跳消息
    public static final class HearbeatHandler extends ChannelInboundHandlerAdapter {
        private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));

        @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);
            }
        }
    }
}

這個示例演示瞭如何使用 IdleStateHandler 來測試遠程節點是否仍然還活着,並且在它 失活時通過關閉連接來釋放資源。

如果連接超過60秒沒有接收或者發送任何的數據,那麼IdleStateHandler 將會使用一個 IdleStateEvent 事件來調用 fireUserEventTriggered()方法。HeartbeatHandler 實現 了 userEventTriggered()方法,如果這個方法檢測到 IdleStateEvent 事件,它將會發送心 跳消息,並且添加一個將在發送操作失敗時關閉該連接的 ChannelFutureListener 。

解碼基於分隔符的協議和基於長度的協議

基於分隔符的協議:
基於分隔符的(delimited)消息協議使用定義的字符來標記的消息或者消息段(通常被稱 爲幀)的開頭或者結尾。由RFC文檔正式定義的許多協議(如SMTP、POP3、IMAP以及Telnet1) 都是這樣的。此外,當然,私有組織通常也擁有他們自己的專有格式。無論你使用什麼樣的協 議,下表中列出的解碼器都能幫助你定義可以提取由任意標記(token)序列分隔的幀的自 定義解碼器。
在這裏插入圖片描述

解碼基於分隔符的協議和基於長度的協議:
在這裏插入圖片描述

//處理由行尾符分隔的幀
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //該LineBasedFrameDecoder將提取的幀轉發給下一個ChannelInboundHandler
        pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
        //添加FrameHandler以接收幀
        pipeline.addLast(new FrameHandle());
    }
    
    public static final class FrameHandle extends SimpleChannelInboundHandler {
        //傳入了單個幀的內容
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
            //Do something with the data exracted from the frame
        }
    }
}

如果你正在使用除了行尾符之外的分隔符分隔的幀,那麼你可以以類似的方式使用 DelimiterBasedFrameDecoder,只需要將特定的分隔符序列指定到其構造函數即可。

這些解碼器是實現你自己的基於分隔符的協議的工具。作爲示例,我們將使用下面的協議規範:

  • 傳入數據流是一系列的幀,每個幀都由換行符(\n)分隔;
  • 每個幀都由一系列的元素組成,每個元素都由單個空格字符分隔;
  • 一個幀的內容代表一個命令,定義爲一個命令名稱後跟着數目可變的參數。

我們用於這個協議的自定義解碼器將定義以下類:

  • Cmd——將幀(命令)的內容存儲在ByteBuf中,一個ByteBuf用於名稱,另一個 用於參數;
  • CmdDecoder——從被重寫了的 decode()方法中獲取一行字符串,並從它的內容構建 一個 Cmd 的實例;
  • CmdHandler——從CmdDecoder獲取解碼的Cmd對象,並對它進行一些處理;
  • CmdHandlerInitializer——爲了簡便起見,我們將會把前面的這些類定義爲專門 的 ChannelInitializer 的嵌套類,其將會把這些 ChannelInboundHandler 安裝
    到 ChannelPipeline 中。
public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
    public static final byte SPACE = (byte)' ';
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加 CmdDecoder 以提取 Cmd 對象,並將它轉發給下 一個 ChannelInboundHandler
        pipeline.addLast(new CmdDecoder(64 * 1024));
        //添加CmdHandler以接收和處理Cmd對象
        pipeline.addLast(new CmdHandler());
    }

    /**
     * Cmd POJO
     */
    public static final class Cmd {
        private final ByteBuf name;
        private final ByteBuf args;

        public Cmd(ByteBuf name, ByteBuf args) {
            this.name = name;
            this.args = args;
        }
        
        public ByteBuf name(){
            return name;
        }
        public ByteBuf args(){
            return args;
        }
    }

    /**
     * Cmd解碼器
     */
    public static final class CmdDecoder extends LineBasedFrameDecoder {

        public CmdDecoder(int maxLength) {
            super(maxLength);
        }

        @Override
        protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
            //從ByteBuf中提取由行尾符序列分隔的幀
            ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
            //如果輸入中沒有幀,則返回null
            if (frame == null) {
                return null;
            }
            //查找第一個空格字符的索引。前面是命令名稱,接着是參數
            int index = frame.indexOf(frame.readerIndex(), frame.writerIndex(), SPACE);
            //使用包含有命令名稱和參數的切片創建新的Cmd對象
            return new Cmd(frame.slice(frame.readerIndex(), index), frame.slice(index + 1, frame.writerIndex()));
        }
    }

    /**
     * CMdHandler
     */
    public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Cmd msg) throws Exception {
            //處理傳經ChannelPipeline的Cmd對象
            //Do something with the command
        }
    }
}

基於長度的協議:
基於長度的協議通過將它的長度編碼到幀的頭部來定義幀,而不是使用特殊的分隔符來標記 它的結束。下表列出了Netty提供的用於處理這種類型的協議的兩種解碼器。
在這裏插入圖片描述

下圖展示了 FixedLengthFrameDecoder 的功能,其在構造時已經指定了幀長度爲 8 字節。
在這裏插入圖片描述
你將經常會遇到被編碼到消息頭部的幀大小不是固定值的協議。爲了處理這種變長幀,你可以使用 LengthFieldBasedFrameDecoder,它將從頭部字段確定幀長,然後從數據流中提取指定的字節數。

示例:長度字段在幀中的偏移量爲 0,並且長度爲 2 字節。
在這裏插入圖片描述

LengthFieldBasedFrameDecoder 提供了幾個構造函數來支持各種各樣的頭部配置情 況。下面的代碼展示瞭如何使用其 3 個構造參數分別爲 maxFrameLength、lengthField- Offset 和 lengthFieldLength 的構造函數。在這個場景中,幀的長度被編碼到了幀起始的前 8 個字節中。

//解碼基於長度的協議
public class LengthBasedInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8));
        //添加FrameHandler以處理每個幀
        pipeline.addLast(new FrameHandler());
    }
    
    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            //處理幀數據
            //Do something with the frame
        }
    }
}

寫大型數據

因爲網絡飽和的可能性,如何在異步框架中高效地寫大塊的數據是一個特殊的問題。由於寫 操作是非阻塞的,所以即使沒有寫出所有的數據,寫操作也會在完成時返回並通知 ChannelFuture。當這種情況發生時,如果仍然不停地寫入,就有內存耗盡的風險。所以在寫大型數據 時,需要準備好處理到遠程節點的連接是慢速連接的情況,這種情況會導致內存釋放的延遲。

NIO 的零拷貝特性這種特性消除了將文件 的內容從文件系統移動到網絡棧的複製過程。所有的這一切都發生在 Netty 的核心中,所以應用 程序所有需要做的就是使用一個 FileRegion 接口的實現,其在 Netty 的 API 文檔中的定義是:
“通過支持零拷貝的文件傳輸的 Channel 來發送的文件區域。”

示例:使用FileRegion傳輸文件的內容

File file = new File("big.txt");
        //創建一個FileInputStream
        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();
                            cause.printStackTrace();
                        }
                    }
                }
        );

這個示例只適用於文件內容的直接傳輸,不包括應用程序對數據的任何處理。在需要將數據 從文件系統複製到用戶內存中時,可以使用 ChunkedWriteHandler,它支持異步寫大型數據 流,而又不會導致大量的內存消耗。

關鍵是interface ChunkedInput<B>,其中類型參數 B 是 readChunk()方法返回的 類型。Netty 預置了該接口的 4 個實現,如下表中所列出的。每個都代表了一個將由 ChunkedWriteHandler 處理的不定長度的數據流。
在這裏插入圖片描述

示例:使用 ChunkedStream 傳輸文件內容
下面代碼說明了 ChunkedStream 的用法,它是實踐中最常用的實現。所示的類使用 了一個 File 以及一個 SslContext 進行實例化。當 initChannel()方法被調用時,它將使用所示的 ChannelHandler 鏈初始化該 Channel。

當 Channel 的狀態變爲活動的時,WriteStreamHandler 將會逐塊地把來自文件中的數 據作爲 ChunkedStream 寫入。數據在傳輸之前將會由 SslHandler 加密。

//使用ChunkedStream傳輸文件內容
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
    private final File file;
    private final SslContext sslContext;

    public ChunkedWriteHandlerInitializer(File file, SslContext sslContext) {
        this.file = file;
        this.sslContext = sslContext;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //添加 ChunkedWriteHandler 以處理作爲 ChunkedInput 傳入的數據
        pipeline.addLast(new SslHandler(sslContext.newEngine(ch.alloc())));
        pipeline.addLast(new ChunkedWriteHandler());
        // 一旦連接建立,WriteStreamHandler就開始寫文件數據
        pipeline.addLast(new WriteStreamHandler());
    }
    
    public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {
        // 當連接建立時,channelActive()方法將使用ChunkedInput寫文件數據
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
        }
    }
}

序列化數據

JDK 提供了 ObjectOutputStream 和 ObjectInputStream,用於通過網絡對 POJO 的 基本數據類型和圖進行序列化和反序列化。該 API 並不複雜,而且可以被應用於任何實現了 java.io.Serializable 接口的對象。但是它的性能也不是非常高效的。

JDK序列化:
如果你的應用程序必須要和使用了ObjectOutputStream和ObjectInputStream的遠 程節點交互,並且兼容性也是你最關心的,那麼JDK序列化將是正確的選擇。下表中列出了 Netty提供的用於和JDK進行互操作的序列化類。

在這裏插入圖片描述

使用JBoss Marshalling進行序列化:

如果你可以自由地使用外部依賴,那麼JBoss Marshalling將是個理想的選擇:它比JDK序列 化最多快 3 倍,而且也更加緊湊。在JBoss Marshalling官方網站主頁 3上的概述中對它是這麼定 義的:

JBoss Marshalling 是一種可選的序列化 API,它修復了在 JDK 序列化 API 中所發現 的許多問題,同時保留了與 java.io.Serializable 及其相關類的兼容性,並添加 了幾個新的可調優參數以及額外的特性,所有的這些都是可以通過工廠配置(如外部序 列化器、類/實例查找表、類解析以及對象替換等)實現可插拔的。

Netty 通過下表所示的兩組解碼器/編碼器對爲 Boss Marshalling 提供了支持。

  • 第一組兼容 只使用 JDK 序列化的遠程節點。
  • 第二組提供了最大的性能,適用於和使用 JBoss Marshalling 的 遠程節點一起使用。
    在這裏插入圖片描述

示例:使用 MarshallingDecoder 和 MarshallingEncoder。同 樣,幾乎只是適當地配置 ChannelPipeline 罷了。

public class MarshallingInitializer extends ChannelInitializer<Channel> {
    private final MarshallerProvider marshallerProvider;
    private final UnmarshallerProvider unmarshallerProvider;

    public MarshallingInitializer(MarshallerProvider marshallerProvider, UnmarshallerProvider unmarshallerProvider) {
        this.marshallerProvider = marshallerProvider;
        this.unmarshallerProvider = unmarshallerProvider;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加 MarshallingDecoder 以 將 ByteBuf 轉換爲 POJO
        pipeline.addLast(new MarshallingDecoder(unmarshallerProvider));
        // 添加 MarshallingEncoder 以將 POJO 轉換爲 ByteBuf
        pipeline.addLast(new MarshallingEncoder(marshallerProvider));
        // 添加 ObjectHandler, 以處理普通的實現了 Serializable 接口的 POJO
        pipeline.addLast(new ObjectHandler());
    }

    public static final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Serializable msg) throws Exception {
            //Do something
        }
    }
}

通過Protocol Buffers序列化:
Netty序列化的最後一個解決方案是利用Protocol Buffers 1的編解碼器,它是一種由Google公 司開發的、現在已經開源的數據交換格式。可以在https://github.com/google/protobuf找到源代碼。

Protocol Buffers 以一種緊湊而高效的方式對結構化的數據進行編碼以及解碼。它具有許多的 編程語言綁定,使得它很適合跨語言的項目。下表展示了 Netty 爲支持 protobuf 所提供的 ChannelHandler 實現。
在這裏插入圖片描述

示例:

<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.11.4</version>
</dependency>
public class ProtoBufIntializer extends ChannelInitializer<Channel> {
    private final MessageLite lite;

    public ProtoBufIntializer(MessageLite lite) {
        this.lite = lite;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加 ProtobufVarint32FrameDecoder 以分隔幀
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //添加 ProtobufEncoder 以處理消息的編碼
        pipeline.addLast(new ProtobufEncoder());
        // 添加 ProtobufDecoder 以解碼消息
        pipeline.addLast(new ProtobufDecoder(lite));
        //添加 Object- Handler 以處 理解碼消息
        pipeline.addLast(new ObjectHandler());
    }
    
    public static final class ObjectHandler extends SimpleChannelInboundHandler<Object> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
            //Do something with the obect
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章