深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?

JJian 發表於 2021-07-27
Netty

前言

  學習Netty避免不了要去了解TCP粘包/拆包問題,熟悉各個編解碼器是如何解決TCP粘包/拆包問題的,同時需要知道TCP粘包/拆包問題是怎麼產生的。

  在此博文前,可以先學習瞭解前幾篇博文:

  參考資料《Netty In Action》、《Netty權威指南》(有需要的小夥伴可以評論或者私信我)

  博文中所有的程式碼都已上傳到Github,歡迎Star、Fork

 


 

一、TCP粘包/拆包

1.什麼是TCP粘包/拆包問題?

引用《Netty權威指南》原話,可以很清楚解釋什麼是TCP粘包/拆包問題。

  TCP是一個“流”協議,是沒有界限的一串資料,TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包問題。

  一個完整的包可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是TCP粘包/拆包。

假設服務端分別傳送兩個資料包P1和P2給服務端,由於服務端讀取一次的位元組數目是不確定的,所以可能會發生五種情況:

              深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?

  • 服務端分兩次讀取到兩個獨立的資料包;
  • 服務端一次接收到兩個資料包,P1和P2粘合在一起,被稱為TCP粘包;
  • 服務端分兩次讀取到兩個資料包,第一次讀取到完整的P1包和P2包的部分內容,第二次讀取到P2包的剩餘內容,被稱之為TCP拆包;
  • 服務端分兩次讀取到兩個資料包,第一次讀取到了P1包的部分內容P1_1,第二次讀取到了P1包的剩餘內容P1_2和P2包的整包
  • 其實還有最後一種可能,就是服務端TCP接收的滑動窗非常小,而資料包P1/P2非常大,很有可能服務端需要分多次才能將P1/P2包接收完全,期間發生多次拆包

2.TCP粘包/拆包問題發生的原因

 TCP是以流動的方式傳輸資料,傳輸的最小單位為一個報文段(segment)。主要有如下幾個指標影響或造成TCP粘包/拆包問題,分別為MSS、MTU、緩衝區,以及Nagle演算法的影響。

(1)MSS(Maximum Segment Size)指的是連線層每次傳輸的資料有個最大限制MTU(Maximum Transmission Unit),超過這個量要分成多個報文段

(2)MTU限制了一次最多可以傳送1500個位元組,而TCP協議在傳送DATA時,還會加上額外的TCP Header和IP Header,因此刨去這兩個部分,就是TCP協議一次可以傳送的實際應用資料的最大大小,即MSS長度=MTU長度-IP Header-TCP Header

(3)TCP為提高效能,傳送端會將需要傳送的資料傳送到緩衝區,等待緩衝區滿了之後,再將緩衝中的資料傳送到接收方。同理,接收方也有緩衝區這樣的機制,來接收資料。

由於有上述的原因,所以會造成拆包/粘包的具體原因如下:

(1)拆包發生原因

  • 要傳送的資料大於TCP傳送緩衝區剩餘空間大小,將會發生拆包
  • 待傳送資料大於MSS(最大報文長度),TCP在傳輸前將進行拆包

(2)粘包發生原因

  • 要傳送的資料小於TCP傳送緩衝區的大小,TCP將多次寫入緩衝區的資料一次傳送出去,將會發生粘包。
  • 接收資料端的應用層沒有及時讀取接收緩衝區中的資料,將發生粘包

二、TCP粘包/拆包問題解決策略

1.常用的解決策略

由於底層TCP是無法理解上層業務資料,所以在底層是無法保證資料包不被拆分和重組的,所以只能通過上層應用協議棧設計來解決

(1)訊息定長,例如每個報文的大小固定長度200位元組,不夠空位補空格

(2)在包尾增加回車換行符進行分割,例如FTP協議

(3)將訊息分為訊息頭和訊息體,訊息頭中包含表示訊息總長度的欄位

(4)更復雜的應用層協議

2.TCP粘包異常問題案例

(1)TimeServerHandler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());


    private int counter;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
        // 每收到一條訊息計數器就加1, 理論上應該接收到100條
        System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                new Date(System.currentTimeMillis()).toString():"BAD ORDER";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(3)TimeServer

public class TimeServer {

    public static final Logger log = LoggerFactory.getLogger(TimeServer.class);

    public static void main(String[] args) throws Exception {
        new TimeServer().bind();
    }


    public void bind() throws Exception {
        // NIO 執行緒組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            // 繫結埠,同步等待成功
            ChannelFuture f = bootstrap.bind(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT).sync();
            log.info("Time server[{}] start success", NettyConstant.REMOTE_IP + ": " + NettyConstant.REMOTE_PORT);
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

(3)TimeClientHandler

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());

    private int counter;
    private byte[] req;

    public TimeClientHandler() {
        req = ("QUERY TIME ORDER" + System.getProperty("line.separator"))
                .getBytes();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf message = null;
        // 迴圈傳送100條訊息,每傳送一條重新整理一次,服務端理論上接收到100條查詢時間指令的請求
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8");
        // 客戶端每接收到服務端一條應答訊息之後,計數器就加1,理論上應該有100條服務端日誌
        System.out.println("Now is: " + body + "; the current is "+ (++counter));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(4)TimeClient

public class TimeClient {

    public static final Logger log = LoggerFactory.getLogger(TimeClient.class);



    public static void main(String[] args) throws Exception {
        new TimeClient().connect(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT);
    }

    public void connect(final String host, final int port) throws Exception {
        // NIO 執行緒組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });

            // 發起非同步連線操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            group.shutdownGracefully();
        }

    }

}

(5)執行測試結果

執行服務端與客戶端,觀察服務端與客戶端

服務端:

The time server receive order: QUERY TIME ORDER
QUERY TIME ORDER
... // 此處忽略96個QUERY TIME ORDER
QUERY TIME ORDER
QUERY TIME ORDER; the counter is : 1

客戶端:

Now is: BAD ORDER
; the current is 1

從結果上來看,客戶端向服務端傳送的100個“QUERY TIME ORDER”命令,都粘成一個包(counter=1),服務端也只返回一個命令“BAD ORDER”,可以嘗試執行客戶端多次,每次執行的結果都是不一樣的,但是大部分都是粘包,計數器都小於了100。

 

三、Netty解決TCP粘包/拆包

1.按行文字解碼器LineBasedFramedDecoder和StringDecoder

  LineBasedFramedDecoder:依次遍歷ByeBuf中可讀位元組,判斷是否有“\n”,“\r\n”,如果有,就當前位置為結束位置,從可讀索引到結束位置區間的位元組就組裝成一行,以換行符為結束標誌的解碼器,同識支援最大長度。

  StringDecoder:將接收物件轉換成字串,然後繼續呼叫後面的handler

  LineBasedFramedDecoder和StringDecoder就是按行切換的文字解碼器,被設計用來支援TCP粘包與拆包

(1)改造TimeServer

  增加解碼器LineBasedFramedDecoder和StringDecoder 

public class TimeServer {

    public static final Logger log = LoggerFactory.getLogger(TimeServer.class);

    public static void main(String[] args) throws Exception {
        new TimeServer().bind();
    }


    public void bind() throws Exception {
        // NIO 執行緒組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            // 繫結埠,同步等待成功
            ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();
            log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

(2)改造TimeServerHandler

  不需要對訊息進行解碼,直接String讀取即可

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());


    private int counter;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 不需要對訊息進行編解碼,直接String讀取
        String body = (String) msg;
        // 每收到一條訊息計數器就加1, 理論上應該接收到100條
        System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                new Date(System.currentTimeMillis()).toString():"BAD ORDER";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(3)改造TimeClient

  同樣增加解碼器LineBasedFramedDecoder和StringDecoder

public class TimeClient {

    public static final Logger log = LoggerFactory.getLogger(TimeClient.class);



    public static void main(String[] args) throws Exception {
        new TimeClient().connect(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT);
    }

    public void connect(final String host, final int port) throws Exception {
        // NIO 執行緒組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });

            // 發起非同步連線操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            group.shutdownGracefully();
        }

    }

}

(4)改造TimeClientHandler

  同樣地,不需要編解碼了,直接返回了字串的應答訊息

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());

    private int counter;
    private byte[] req;

    public TimeClientHandler() {
        req = ("QUERY TIME ORDER" + System.getProperty("line.separator"))
                .getBytes();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf message = null;
        // 迴圈傳送100條訊息,每傳送一條重新整理一次,服務端理論上接收到100條查詢時間指令的請求
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 不需要編解碼了,直接返回了字串的應答訊息
        String body = (String) msg;
        // 客戶端每接收到服務端一條應答訊息之後,計數器就加1,理論上應該有100條服務端日誌
        System.out.println("Now is: " + body + "; the current is "+ (++counter));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(5)執行測試結果

服務端:

The time server receive order: QUERY TIME ORDER; the counter is : 1
The time server receive order: QUERY TIME ORDER; the counter is : 2
...
The time server receive order: QUERY TIME ORDER; the counter is : 99
The time server receive order: QUERY TIME ORDER; the counter is : 100

客戶端:

Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 2
...
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 99
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100

根據結果可知,每條訊息都對計數器加1,並沒有發生粘包現象。

2.按分隔符文字解碼器DelimiterBasedFrameDecoder

  DelimiterBasedFrameDecoder是以分隔符作為碼流結束標識的訊息解碼,改造程式碼,以“$_”作為分隔符

(1)改造TimeServer

  增加以“$_”為分隔符的DelimiterBasedFrameDecoder解碼器,DelimiterBasedFrameDecoder構造器其中第一個引數長度表示當達到該長度後仍然沒有查詢到分隔符,就會丟擲TooLongFrameException。這是防止異常碼流缺失分隔符導致記憶體溢位

public class TimeServer {

    public static final Logger log = LoggerFactory.getLogger(TimeServer.class);

    public static void main(String[] args) throws Exception {
        new TimeServer().bind();
    }


    public void bind() throws Exception {
        // NIO 執行緒組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            // 以“$_”為分隔符
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                            socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            // 繫結埠,同步等待成功
            ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();
            log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

(2)改造TimeServerHandler

  對返回客戶端的訊息增加分隔符“$_”

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());


    private int counter;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 不需要對訊息進行編解碼,直接String讀取
        String body = (String) msg;
        // 每收到一條訊息計數器就加1, 理論上應該接收到100條
        System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                new Date(System.currentTimeMillis()).toString():"BAD ORDER";
        // 返回客戶端需要追加分隔符
        currentTime = currentTime + "$_";
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(3)改造TimeClient

  增加以“$_”為分隔符的DelimiterBasedFrameDecoder解碼器

public class TimeClient {

    public static final Logger log = LoggerFactory.getLogger(TimeClient.class);

    public static void main(String[] args) throws Exception {
        new TimeClient().connect(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT);
    }

    public void connect(final String host, final int port) throws Exception {
        // NIO 執行緒組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 以“$_”為分隔符
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                            socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });

            // 發起非同步連線操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            group.shutdownGracefully();
        }

    }

}

(4)改造TimeClientHandler

  對傳送命令增加“$_”分隔符

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());

    private int counter;
    private byte[] req;

    public TimeClientHandler() {
        // 以$_為分隔符,傳送命令
        req = ("QUERY TIME ORDER$_").getBytes();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf message = null;
        // 迴圈傳送100條訊息,每傳送一條重新整理一次,服務端理論上接收到100條查詢時間指令的請求
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 不需要編解碼了,直接返回了字串的應答訊息
        String body = (String) msg;
        // 客戶端每接收到服務端一條應答訊息之後,計數器就加1,理論上應該有100條服務端日誌
        System.out.println("Now is: " + body + "; the current is "+ (++counter));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(5)執行測試結果

服務端:

The time server receive order: QUERY TIME ORDER; the counter is : 1
The time server receive order: QUERY TIME ORDER; the counter is : 2
...
The time server receive order: QUERY TIME ORDER; the counter is : 99
The time server receive order: QUERY TIME ORDER; the counter is : 100

客戶端:

Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 2
...
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 99
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100

根據結果可知,每條訊息都對計數器加1,並沒有發生粘包現象。

3.固定長度解碼器FixedLengthFrameDecoder

  FixedLengthFrameDecoder是固定長度解碼器,能夠對固定長度的訊息進行自動解碼,利用FixedLengthFrameDecoder,無論多少資料,都會按照建構函式中設定的固定長度進行解碼,如果是半包訊息,FixedLengthFrameDecoder會快取半包訊息並等待下一個包到達後進行拼包,直到讀取到一個完整的包

  在服務端ChannelPipeline中新增FixedLengthFrameDecoder,長度為10。然後增加EchoServerHannel處理器,輸出服務端接收到的命令

(1)EchoServer

  增加長度為10的FixedLengthFrameDecoder解碼器,同時再增加StringDecoder解碼器

public class EchoServer {

    public static final Logger log = LoggerFactory.getLogger(EchoServer.class);

    public static void main(String[] args) throws Exception {
        new EchoServer().bind();
    }


    public void bind() throws Exception {
        // NIO 執行緒組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            // 增加固定長度解碼器
                            socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(10));
                            // 增加字元解碼器,將msg直接轉為string
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            // 繫結埠,同步等待成功
            ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();
            log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);
            // 等待所有服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

(2)EchoServerHandler

  輸出客戶端傳送的命令,直接輸出msg即可,因為服務端已經增加了StringDecoder解碼器,直接轉為String

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = Logger.getLogger(EchoServerHandler.class.getName());

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("The time server receive order: " + msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

(3)Telnet命令測試結果

CMD視窗Telnet視窗連線 telnet 127.0.0.1 8888

深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?

回顯輸入訊息welcome Lijian

深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?

檢視服務端console

2021-07-26 23:25:21,921  INFO [nioEventLoopGroup-2-1] - [id: 0xe4d49ee6, L:/127.0.0.1:8888] READ: [id: 0x928b38a4, L:/127.0.0.1:8888 - R:/127.0.0.1:62315]
2021-07-26 23:25:21,922  INFO [nioEventLoopGroup-2-1] - [id: 0xe4d49ee6, L:/127.0.0.1:8888] READ COMPLETE
The time server receive order: welcome Li

根據結果可知,服務端只接收到客戶端傳送的“welcome Lijian”的前10個字元,及說明FixedLengthFrameDecoder是有效的

 

本篇博文是Netty的基礎篇,主要介紹Netty針解決TCP粘包/拆包而產生的解碼器,Netty基礎篇還涉及到序列化的問題,後面將會繼續介紹。