Netty - 粘包與拆包

伊邪那岐丶發表於2020-10-30

粘包和拆包

產生粘包和拆包問題的主要原因是,作業系統在傳送TCP資料的時候,底層會有一個緩衝區,例如1024個位元組大小,如果一次請求傳送的資料量比較小,沒達到緩衝區大小,TCP則會將多個請求合併為同一個請求進行傳送,這就形成了粘包問題;如果一次請求傳送的資料量比較大,超過了緩衝區大小,TCP就會將其拆分為多次傳送,這就是拆包,也就是將一個大的包拆分為多個小包進行傳送。

問題重現

服務端

NIOServer.java

/**
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-26 20:42
 */
public class NIOServer {

    @SneakyThrows
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss,work);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                pipeline.addLast(new MyServerHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.bind("127.0.0.1", 9090).sync();
        channelFuture.channel().closeFuture().sync();
    }

}

主要看自己寫的處理器類MyServerHandler.java

/**
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-26 20:50
 */
public class MyServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        SocketAddress socketAddress = channelHandlerContext.channel().remoteAddress();
        System.out.println(socketAddress + "----" + s.trim());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketAddress socketAddress = ctx.channel().remoteAddress();
        System.out.println(socketAddress+"---已連線!");
    }

}

客戶端

NIOClient.java

/**
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-26 20:52
 */
public class NIOClient {

    @SneakyThrows
    public static void main(String[] args) {
        EventLoopGroup client = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(client);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<SocketChannel>(){
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                pipeline.addLast(new MyClientHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9090).sync();
        Channel channel = channelFuture.channel();

        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset()));
        while (true){
            String message = reader.readLine().trim();
            for (int i = 0; i < 10; i++) {
                channel.writeAndFlush(message);
            }
        }
    }

}

MyClientHandler.java

/**
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-26 21:02
 */
public class MyClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        SocketAddress socketAddress = channelHandlerContext.channel().remoteAddress();
        System.out.println(socketAddress + "發來訊息:  " + s);
    }
}

理想的情況.我們輸入一段文字服務端就應該列印這一段文字出來…

結果圖

在這裡插入圖片描述
在這裡插入圖片描述

可以發現資料並不是一段一段的列印出來.
netty自己封裝了一次java.nio的ByteBuffer使用的是ByteBuf.可以自動擴容.
緩衝區,例如1024個位元組大小,如果一次請求傳送的資料量比較小,沒達到緩衝區大小,TCP則會將多個請求合併為同一個請求進行傳送,這就形成了粘包問題.

解決方案

對於粘包和拆包問題,常見的解決方案有四種

(1)通過FixedLengthFrameDecoder 定長解碼器來解決定長訊息的黏包問題;

(2)通過LineBasedFrameDecoder和StringDecoder來解決以回車換行符作為訊息結束符的TCP黏包的問題;

(3)通過DelimiterBasedFrameDecoder 特殊分隔符解碼器來解決以特殊符號作為訊息結束符的TCP黏包問題;

(4)最後一種,也是本文的重點,通過LengthFieldBasedFrameDecoder 自定義長度解碼器解決TCP黏包問題。

NIOClient.java

public class NIOClient {

    @SneakyThrows
    public static void main(String[] args) {
        EventLoopGroup client = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(client);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<SocketChannel>(){
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                /**
                 * 1) lengthFieldOffset  //長度欄位的偏差
                 * 2) lengthFieldLength  //長度欄位佔的位元組數
                 * 3) lengthAdjustment  //新增到長度欄位的補償值
                 * 4) initialBytesToStrip  //從解碼幀中第一次去除的位元組數
                 */
                pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));

                /**
                 * 計算當前待傳送訊息的二進位制位元組長度,將該長度新增到ByteBuf的緩衝區頭中
                 */
                pipeline.addLast(new LengthFieldPrepender(4));
                pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                pipeline.addLast(new MyClientHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9090).sync();
        Channel channel = channelFuture.channel();

        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset()));
        while (true){
            String message = reader.readLine().trim();
            for (int i = 0; i < 10; i++) {
                channel.writeAndFlush(message);
            }
        }
    }

}

NIOServer.java

public class NIOServer {

    @SneakyThrows
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss,work);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                /**
                 * 1) lengthFieldOffset  //長度欄位的偏差
                 * 2) lengthFieldLength  //長度欄位佔的位元組數
                 * 3) lengthAdjustment  //新增到長度欄位的補償值
                 * 4) initialBytesToStrip  //從解碼幀中第一次去除的位元組數
                 */
                pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));

                /**
                 * 計算當前待傳送訊息的二進位制位元組長度,將該長度新增到ByteBuf的緩衝區頭中
                 */
                pipeline.addLast(new LengthFieldPrepender(4));
                pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                pipeline.addLast(new MyServerHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.bind("127.0.0.1", 9090).sync();
        channelFuture.channel().closeFuture().sync();
    }

}

在這裡插入圖片描述

在這裡插入圖片描述

問題解決!

相關文章