Netty入站出站介紹及藉助使用到責任鏈簡單實現實現網絡編程中拆包和粘包

Netty入站出站事件

入站事件:
通常指I/O線程生成入站數據。(通俗理解:從socket底層往上冒的事件都是入站)比如EventLoop收到selector的OP_READ事件,入站處理器調用socketChannel.read(ByteBuffer)接收到數據後,這將通道ChannelPipeline中包含的下一個channelRead方法被調用。

在這裏插入圖片描述

出站事件:
經常是指I/O線程執行實際的輸出操作。(通俗理解:主動往socket底層操作的事件都是出站)比如bind方法的用意是請求server socket綁定的SocketAddress,這將導致通道的ChannelPipline包含下一個出站處理器中bind方法被調用。
在這裏插入圖片描述

Netty通過責任鏈實現出入站事件處理

我們在使用責任鏈的時候更多的是
在這裏插入圖片描述
netty的責任鏈,與我們我們經常使用到責任鏈相比多了ChannelPipeline來維護這個責任鏈。
ChannelPipeline鏈中存儲的也並不是直接的處理的處理器,而是包裹handler處理器的DefaultChannelHandlerContext。
對於我這邊理解之所以引入ChannelPipeline這個上下文,是因爲在在使用handler處理器的時候,需要有添加和刪除handler的操作,用ChannelPipeline這個這個責任鏈能夠更好管理hanler處理。

DefaultChannelHandlerContext這種包裝的代替handler的原因,是在handler處理的時候,可能會經常遇到另外線程池的處理,而更好關注業務本身用DefaultChannelHandlerContext代替了handler.
在這裏插入圖片描述

在這裏插入圖片描述

那我之所以需要分入站與出站,這個是因爲我們在添加責任鏈的時候,入站事件只會處理入站事件,而出站同樣也只能處理出站事件。
在這裏插入圖片描述

藉助責任鏈簡單解決一下網絡編程中拆包和粘包

拆包和粘包出現的原因:
(1)由於Nagle算法可能會將一些,要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包;

(2)接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包;

(3)要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包;

(4)待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。即TCP報文長度-TCP頭部長度>MSS。

拆包和粘包出現的解決方案:
由於底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,歸納如下:

(1)、使用帶消息頭的協議、消息頭存儲消息開始標識及消息長度信息,服務端獲取消息頭的時候解析出消息長度,然後向後讀取該長度的內容。

(2)、設置定長消息,服務端每次讀取既定長度的內容作爲一條完整消息。

(3)、設置消息邊界,服務端從網絡流中按消息編輯分離出消息內容。

Netty自帶的拆包器:
(1)、固定長度的拆包器FixedLengthFrameDecoder:如果應用層協議非常簡單,每個數據包的長度都是固定的,需要把拆包器加到Pipeline,Netty把指定長度的數據包(ByteBuf)傳遞到下一個ChannelHandler.
(2)、行拆包器LineBasedFrameDecoder:發送端發送數據包的時候,每個數據包之間以換行符作爲分隔,接收端通過LineBasedFrameDecoder將粘包的ByteBuf拆分成一個個完整的應用層數據包.
(3)、分隔符拆包器DelimiterBasedFrameDecoder:行拆包器的通用版本,自定義分隔符.
(4)、基於長度域拆包器LengthFieldBasedFrameDecoder:最通用的一種拆包器,只要自定義協議包含長度域字段均使用此拆包器實現應用層拆包。使用LengthFieldBasedFrameDecoder需要長度域相對整個數據包的偏移量和長度域的長度構造拆包器,Pipeline最前面添加此拆包器。

代碼編寫:
1、這裏通過最簡單 固定發送長度這種最簡單的粗略的方案來演示這個問題與處理。這裏採用每次發送210個字節。

服務端代碼:

public class XNettyServer {
    public static void main(String[] args) throws Exception {
        // 1、 線程定義
        // accept 處理連接的線程池線程大小爲默認當前cpu的兩倍
        //DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
        EventLoopGroup acceptGroup = new NioEventLoopGroup();
        // read io 處理數據的線程池
        EventLoopGroup readGroup = new NioEventLoopGroup();
        //業務線程,防止處理線程的任務因爲任務阻塞
        NioEventLoopGroup bizGroup = new NioEventLoopGroup(4,new DefaultThreadFactory("biz-thread-pool-"));
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(acceptGroup, readGroup);
            // 2、 選擇TCP協議,NIO的實現方式
            b.channel(NioServerSocketChannel.class);
           // ChannelOption.SO_KEEPALIVE表示是否開啓TCP底層心跳機制,true爲開啓
             b.childOption(ChannelOption.SO_KEEPALIVE, true);
             //ChannelOption.SO_REUSEADDR表示端口釋放後立即就可以被再次使用,因爲一般來說,一個端口釋放後會等待兩分鐘之後才能再被使用
             b.childOption(ChannelOption.SO_REUSEADDR, true);
             // ChannelOption.TCP_NODELAY表示是否開始Nagle算法,true表示關閉,false表示開啓,通俗地說,如果要求高實時性,有數據發送時就馬上發送,就關閉,如果需要減少發送次數減少網絡交互就開啓
            b.childOption(ChannelOption.TCP_NODELAY, false);
            //設置發送緩衝區大小
          //  b.childOption(ChannelOption.SO_SNDBUF, 32 * 1024);
            //設置接收緩衝區大小
          //  b.childOption(ChannelOption.SO_RCVBUF, 32 * 1024);
            XHandller xHandller = new XHandller();
            b.childHandler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 3、 職責鏈定義(請求收到後怎麼處理)
                    ChannelPipeline pipeline = ch.pipeline();

                    pipeline.addLast(new XDecoder());

                    pipeline.addLast(bizGroup,xHandller);
                }
            });
            // 4、 綁定端口
            System.out.println("啓動成功,端口 9999");
            b.bind(9999).sync().channel().closeFuture().sync();
        } finally {
            acceptGroup.shutdownGracefully();
            readGroup.shutdownGracefully();
        }
    }
}

根據定義每次發送210個字節來進行解碼

public class XDecoder extends ByteToMessageDecoder {
    static final int PACKET_SIZE = 220;

    // 用來臨時保留沒有處理過的請求報文
    ByteBuf tempMsg = Unpooled.buffer();

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("收到了一次數據包,長度是:" + in.readableBytes());
        // in 請求的數據
        // out 將粘在一起的報文拆分後的結果保留起來

        // 1、 合併報文
        ByteBuf message = null;
        int tmpMsgSize = tempMsg.readableBytes();
        // 如果暫存有上一次餘下的請求報文,則合併
        if (tmpMsgSize > 0) {
            message = Unpooled.buffer();
            message.writeBytes(tempMsg);
            message.writeBytes(in);
            System.out.println("合併:上一數據包餘下的長度爲:" + tmpMsgSize + ",合併後長度爲:" + message.readableBytes());
        } else {
            message = in;
        }

        // 2、 拆分報文
   
        int size = message.readableBytes();
        int counter = size / PACKET_SIZE;
        for (int i = 0; i < counter; i++) {
            byte[] request = new byte[PACKET_SIZE];
            // 每次從總的消息中讀取210個字節的數據
            message.readBytes(request);

            // 將拆分後的結果放入out列表中,交由後面的業務邏輯去處理
            out.add(Unpooled.copiedBuffer(request));
        }

        // 3、多餘的報文存起來
        // 第一個報文: i+  暫存
        // 第二個報文: 1 與第一次
        size = message.readableBytes();
        if (size != 0) {
            System.out.println("多餘的數據長度:" + size);
            // 剩下來的數據放到tempMsg暫存
            tempMsg.clear();
            tempMsg.writeBytes(message.readBytes(size));
        }

    }

}

業務處理器:

/**
 * 後續處理handdler
 */
@ChannelHandler.Sharable
public class XHandller extends ChannelInboundHandlerAdapter {

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 輸出 bytebuf
        ByteBuf buf = (ByteBuf) msg;
        byte[] content = new byte[buf.readableBytes()];
        buf.readBytes(content);

        System.out.println(Thread.currentThread().getName() + ":" + new String(content));
        //釋放堆外內存 重複利用,gc釋放
       // buf.release(); //也快交流後面處理緩衝區的釋放
        ctx.fireChannelRead(msg);
    }

    // 異常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

客戶端:

public class TonySocketClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 9999);
        OutputStream outputStream = socket.getOutputStream();

        // 消息長度固定爲 220字節,包含有
        // 2. 消息內容字符串長度最多70。 按一個漢字3字節,內容的最大長度爲210字節
        byte[] request = new byte[220];
        byte[] userId = "useridtest".getBytes();
        byte[] content = "發送內容test23444444444444444444444444445test".getBytes();
        System.arraycopy(userId, 0, request, 0, 10);
        System.arraycopy(content, 0, request, 10, content.length);
		//通過併發方式來展示發送消息,這樣更容易出現粘包
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    outputStream.write(request);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();
        Thread.sleep(2000L); // 兩秒後退出
        socket.close();
    }
}

處理結果:
在這裏插入圖片描述
當我註釋掉添加解嗎處理器:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章