Netty(7)-構建pipeline、channelHandler、Decoder和Encoder

一.pipeline和channelHandler

使用pipeline,把複雜的邏輯從單獨的一個 channelHandler 中抽取出來,pipeline構造成一個雙向鏈表將一個個channelHandler串聯起來,形成邏輯處理責任鏈,每個hanlder只負責處理各自的協議包:

在這裏插入圖片描述

1.channelHandler的分類

在這裏插入圖片描述
ChannelHandler 有兩大子接口:

1.ChannelInboundHandler

ChannelInboundHandler用以處理讀數據的邏輯,比如,我們在一端讀到一段數據,首先要解析這段數據,然後對這些數據做一系列邏輯處理,最終把響應寫到對端, 在開始組裝響應之前的所有的邏輯,都可以放置在 ChannelInboundHandler 裏處理,它的一個最重要的方法就是 channelRead()。

2.ChannelOutBoundHandler

ChannelOutBoundHandler處理寫數據的邏輯,它是定義我們一端在組裝完響應之後,把數據寫到對端的邏輯,比如,我們封裝好一個 response 對象,接下來我們有可能對這個 response 做一些其他的特殊邏輯,然後,再編碼成 ByteBuf,最終寫到對端,它裏面最核心的一個方法就是 write()。

這兩個子接口分別有對應的默認實現,ChannelInboundHandlerAdapter,和 ChanneloutBoundHandlerAdapter,它們分別實現了兩大接口的所有功能,默認情況下會把讀寫事件傳播到下一個 handler。

2.channelHandler的事件傳播

在服務端的 pipeline 添加三個 ChannelInboundHandler

  • NettyServer
serverBootstrap
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
            protected void initChannel(NioSocketChannel ch) {
                // inBound,處理讀數據的邏輯鏈
                ch.pipeline().addLast(new InBoundHandlerA());
                ch.pipeline().addLast(new InBoundHandlerB());
                ch.pipeline().addLast(new InBoundHandlerC());
                
                // outBound,處理寫數據的邏輯鏈
                ch.pipeline().addLast(new OutBoundHandlerA());
                ch.pipeline().addLast(new OutBoundHandlerB());
                ch.pipeline().addLast(new OutBoundHandlerC());
            }
        });
  • InBoundHandler
public class InBoundHandlerA extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerA: " + msg);
        super.channelRead(ctx, msg);
    }
}

public class InBoundHandlerB extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerB: " + msg);
        super.channelRead(ctx, msg);
    }
}

public class InBoundHandlerC extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerC: " + msg);
        super.channelRead(ctx, msg);
    }
}
  • OutBounddHandler
public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerA: " + msg);
        super.write(ctx, msg, promise);
    }
}

public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerB: " + msg);
        super.write(ctx, msg, promise);
    }
}

public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerC: " + msg);
        super.write(ctx, msg, promise);
    }
}

執行程序

在這裏插入圖片描述

可以看到,inBoundHandler的執行順序與我們添加的順序相同,而outBoundHandler的執行順序與我們添加的順序相反。

3.pipeline的結構與channelHandler的執行順序

  1. pipeline結構
    在這裏插入圖片描述
  2. channelHandler執行順序
    在這裏插入圖片描述

二.構建客戶端和服務端的pipeline與Decoder和Encoder

Netty 內置了很多開箱即用的 ChannelHandler。下面,我們通過學習 Netty 內置的 ChannelHandler 來逐步構建我們的 pipeline。

1.ChannelInboundHandlerAdapter 與 ChannelOutboundHandlerAdapter

首先是 ChannelInboundHandlerAdapter ,這個適配器主要用於實現其接口 ChannelInboundHandler 的所有方法,這樣我們在編寫自己的 handler 的時候就不需要實現 handler 裏面的每一種方法,而只需要實現我們所關心的方法,默認情況下,對於 ChannelInboundHandlerAdapter,我們比較關心的是他的如下方法

  • ChannelInboundHandlerAdapter
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.fireChannelRead(msg);
}

他的作用就是接收上一個 handler 的輸出,這裏的 msg 就是上一個 handler 的輸出。大家也可以看到,默認情況下 adapter 會通過 fireChannelRead() 方法直接把上一個 handler 的輸出結果傳遞到下一個 handler。(責任鏈模式)

與 ChannelInboundHandlerAdapter 類似的類是 ChannelOutboundHandlerAdapter,它的核心方法如下

  • ChannelOutboundHandlerAdapter
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}

默認情況下,這個 adapter 也會把對象傳遞到下一個 outBound 節點,它的傳播順序與 inboundHandler 相反,這裏就不再對這個類展開了。

2.Decoder與Encoder

1.PacketDecoder

通常情況下,無論我們是在客戶端還是服務端,當我們收到數據之後,首先要做的事情就是把二進制數據轉換到我們的一個 Java 對象,所以 Netty 很貼心地寫了一個父類,來專門做這個事情,下面我們來看一下,如何使用這個類來實現服務端的解碼

public class PacketDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
        out.add(PacketCodeC.INSTANCE.decode(in));
    }
}

當我們繼承了 ByteToMessageDecoder 這個類之後,我們只需要實現一下 decode() 方法,這裏的 in 大家可以看到,傳遞進來的時候就已經是 ByteBuf 類型,所以我們不再需要強轉,第三個參數是 List 類型,我們通過往這個 List 裏面添加解碼後的結果對象,就可以自動實現結果往下一個 handler 進行傳遞,這樣,我們就實現瞭解碼的邏輯 handler。

另外,值得注意的一點,對於 Netty 裏面的 ByteBuf,我們使用 4.1.6.Final 版本,默認情況下用的是堆外內存,在 ByteBuf 這一小節中我們提到,堆外內存我們需要自行釋放,在我們前面小節的解碼的例子中,其實我們已經漏掉了這個操作,這一點是非常致命的,隨着程序運行越來越久,內存泄露的問題就慢慢暴露出來了, 而這裏我們使用 ByteToMessageDecoder,Netty 會自動進行內存的釋放

當我們通過解碼器把二進制數據轉換到 Java 對象即指令數據包之後,就可以針對每一種指令數據包編寫邏輯了。

2.PacketEncoder

處理完請求之後,我們都會給客戶端一個響應,在寫響應之前,我們需要把響應對象編碼成 ByteBuf。

  • LoginRequestHandler
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        LoginResponsePacket loginResponsePacket = login(loginRequestPacket);
        ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginResponsePacket);
        ctx.channel().writeAndFlush(responseByteBuf);
    }
}
  • MessageRequestHandler
public class MessageRequestHandler extends SimpleChannelInboundHandler<MessageRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageRequestPacket messageRequestPacket) {
        MessageResponsePacket messageResponsePacket = receiveMessage(messageRequestPacket);
        ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), messageRequestPacket);
        ctx.channel().writeAndFlush(responseByteBuf);
    }
}

我們注意到,我們處理每一種指令完成之後的邏輯是類似的,都需要進行編碼,然後調用 writeAndFlush() 將數據寫到客戶端,這個編碼的過程其實也是重複的邏輯,而且在編碼的過程中,我們還需要手動去創建一個 ByteBuf,如下過程

  • PacketUtil.java
public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet) {
    // 1. 創建 ByteBuf 對象
    ByteBuf byteBuf = byteBufAllocator.ioBuffer();
    // 2. 序列化 java 對象

    // 3. 實際編碼過程

    return byteBuf;
}

而Netty 提供了一個特殊的 channelHandler 來專門處理編碼邏輯,我們不需要每一次將響應寫到對端的時候調用一次編碼邏輯進行編碼,也不需要自行創建 ByteBuf,這個類叫做 MessageToByteEncoder,從字面意思也可以看出,它的功能就是將對象轉換到二進制數據。

  • PacketEncoder
public class PacketEncoder extends MessageToByteEncoder<Packet> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf out) {
        PacketUtil.INSTANCE.encode(out, packet);
    }
}
  • PackageUtil
/**
 * @Auther: ARong
 * @Date: 2020/2/4 9:14 下午
 * @Description: 用於包編碼和解碼
 */
public class PacketUtil {

    // 餓漢單例模式
    public static PacketUtil INSTANCE = new PacketUtil();

    private PacketUtil(){}
    /*
     * @Author ARong
     * @Description 將packet編碼爲byteBuf
     * @Date 2020/2/9 9:04 下午
     * @Param [byteBuf, packet]
     * @return void
     **/
    public void encode(ByteBuf byteBuf, Packet packet) {
        // 獲取序列化器序列化對象
        MySerializer serializer = SerializerFactory.getSerializer(packet.getSerMethod());
        byte[] data = serializer.serialize(packet);
        // 按照通信協議填充ByteBUf
        byteBuf.writeInt(packet.getMagic());// 魔數
        byteBuf.writeByte(packet.getVersion()); // 版本號
        byteBuf.writeByte(packet.getSerMethod()); // 序列化方式
        byteBuf.writeByte(packet.getCommand()); // 指令
        byteBuf.writeInt(data.length);// 數據長度
        byteBuf.writeBytes(data); // 數據
    }

    /*
     * @Author ARong
     * @Description 將ByteBuf按照約定序列化方式解碼成Packet
     * @Date 2020/2/4 9:21 下午
     * @Param [byteBuf]
     * @return io_learn.netty.part4_protocol.packet.Packet
     **/
    public Packet decode(ByteBuf byteBuf) {
        // 暫不判斷魔數,跳過
        byteBuf.skipBytes(4);
        // 暫不判斷魔數版本號,跳過
        byteBuf.skipBytes(1);
        // 獲取序列化方式
        byte serMethod = byteBuf.readByte();
        // 獲取指令
        byte command = byteBuf.readByte();
        // 獲取數據包長度
        int length = byteBuf.readInt();
        // 獲取存儲數據的字節數組
        byte[] data = new byte[length];
        byteBuf.readBytes(data);
        // 反序列化數據,獲取Packet
        Class<? extends Packet> packetType = getPacketType(command);
        Packet res = SerializerFactory.getSerializer(serMethod).deserialize(packetType, data);
        return res;
    }


    /*
     * @Author ARong
     * @Description 通過指令獲取相應的Packet
     * @Date 2020/2/4 9:31 下午
     * @Param [commond]
     * @return io_learn.netty.part4_protocol.packet.Packet
     **/
    public static Class<? extends Packet> getPacketType(byte commond) {
        if (commond == Command.LOGIN_REQUEST) {
            return LoginRequestPacket.class;
        }
        if (commond == Command.LOGIN_RESPONSE) {
            return LoginResponsePacket.class;
        }
        if (commond == Command.MESSAGE_REQUEST) {
            return MessageRequestPacket.class;
        }
        if (commond == Command.MESSAGE_RESPONSE) {
            return MessageResponsePacket.class;
        }
        return null;
    }
}

之後就可以這樣使用,而無需再次編碼了:

public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        ctx.channel().writeAndFlush(login(loginRequestPacket));
    }
}

3.SimpleChannelInboundHandler

回顧一下我們前面處理 Java 對象的邏輯

if (packet instanceof LoginRequestPacket) {
    // ...
} else if (packet instanceof MessageRequestPacket) {
    // ...
} else if ...

我們通過 if else 邏輯進行邏輯的處理,當我們要處理的指令越來越多的時候,代碼會顯得越來越臃腫,我們可以通過給 pipeline 添加多個 handler(ChannelInboundHandlerAdapter的子類) 來解決過多的 if else 問題,如下

  • XXXHandler.java
if (packet instanceof XXXPacket) {
    // ...處理
} else {
    // 責任鏈模式
   ctx.fireChannelRead(packet); 
}

這樣一個好處就是,每次添加一個指令處理器,邏輯處理的框架都是一致的,

但是,大家應該也注意到了,這裏我們編寫指令處理 handler 的時候,依然編寫了一段我們其實可以不用關心的 if else 判斷,然後還要手動傳遞無法處理的對象 (XXXPacket) 至下一個指令處理器,這也是一段重複度極高的代碼,因此,Netty 基於這種考慮抽象出了一個 SimpleChannelInboundHandler 對象,類型判斷和對象傳遞的活都自動幫我們實現了,而我們可以專注於處理我們所關心的指令即可。

下面,我們來看一下如何使用 SimpleChannelInboundHandler 來簡化我們的指令處理邏輯

  • LoginRequestHandler
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        // 登錄邏輯
    }
}

SimpleChannelInboundHandler 從字面意思也可以看到,使用它非常簡單,我們在繼承這個類的時候,給他傳遞一個泛型參數,然後在 channelRead0() 方法裏面,我們不用再通過 if 邏輯來判斷當前對象是否是本 handler 可以處理的對象,也不用強轉,不用往下傳遞本 handler 處理不了的對象,這一切都已經交給父類 SimpleChannelInboundHandler 來實現了,我們只需要專注於我們要處理的業務邏輯即可。

是用來處理登錄的邏輯,同理,我們可以很輕鬆地編寫一個消息處理邏輯處理器

  • MessageRequestHandler
public class MessageRequestHandler extends SimpleChannelInboundHandler<MessageRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageRequestPacket messageRequestPacket) {

    }
}

對應我們的代碼

  • NettyServer
serverBootstrap
               .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new PacketDecoder());
                        ch.pipeline().addLast(new LoginRequestHandler());
                        ch.pipeline().addLast(new MessageRequestHandler());
                        ch.pipeline().addLast(new PacketEncoder());
                    }
            });

客戶端

  • NettyClient
bootstrap
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) {
                ch.pipeline().addLast(new PacketDecoder());
                ch.pipeline().addLast(new LoginResponseHandler());
                ch.pipeline().addLast(new MessageResponseHandler());
                ch.pipeline().addLast(new PacketEncoder());
            }
        });
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章