Netty中的粘包和拆包問題

TCP中的粘包和拆包


簡述TCP的粘包和拆包

TCP編程中無論是服務端還是客戶端,讀取和發送消息時都要考慮TCP底層的粘包和拆包機制,TCP是一個‘流’協議,數據是沒有界限的,TCP底層不知道上層業務數據的含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以相對於業務來說,一個完整的包可能會被TCP拆分多個包進行發送 ,也有可能把許多小的包封裝成一個大的數據包發送,這就是TCP的粘包和拆包的問題

粘包、拆包問題說明
現在假設客戶端向服務端連續發送了兩個數據包,用packet1和packet2來表示,那麼服務端收到的數據可以分爲三種,如下所示:
第一種情況,接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不考慮。

第二種情況,接收端只收到一個數據包,由於TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即爲粘包。這種情況由於接收端不知道這兩個數據包的界限,所以接收端不知道如何處理。

第三種情況,這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要麼是不完整的,要麼就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。

TCP粘包拆包發生的原因有很多,主要包括如下:

  • 要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。
  • 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
  • 要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。
  • 接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。

粘包和拆包的解決策略

由於TCP無法知道上層業務數據,所以TCP底層無法保證數據包不會被拆分和重組,所以我們只能利用上層的應用協議棧設計來解決,歸納如下:

  1. 消息定長,例如每個報文的大小爲固定長度200字節,如果不夠,空位補空格
  2. 在包尾增加回車換行符進行分割,例如FTP協議
  3. 將消息分爲消息頭和消息體,消息頭包含消息的總長度(或者消息體長度)

以上3種方式,客戶端接受到包的時候就可以根據這些約束區分出來不同的包。

 

Netty中解決TCP粘包拆包問題

爲了解決TCP中粘包、拆包導致的半包讀寫問題,Netty默認提供了多種編解碼器用於處理半包,直接使用這些類庫,TCP粘包拆包問題就變得非常容易

LineBasedFremeDecoder解決TCP粘包問題

LineBasedFremeDecoder改造服務端代碼

public class NettyServer {

    public void bind(int port){
        //NioEventLoopGroup是一個線程組,包含一組NIO線程
        EventLoopGroup bossGroup = new NioEventLoopGroup();//用於服務端接受客戶端的連接
        EventLoopGroup workerGroup = new NioEventLoopGroup();//用於SocketChannel的網絡讀寫
        try{
            //ServerBootstrap對象是Netty用於啓動NIO服務端的輔助啓動類
            ServerBootstrap bs = new ServerBootstrap();
            bs.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)//設置創建的channel
                    .option(ChannelOption.SO_BACKLOG, 1024)//設置NioServerSocketChannel的TCP參數
                    .childHandler(new ChildChannelHandler());//綁定I/O事件處理類
            ChannelFuture sync = bs.bind(port).sync();//綁定監聽端口並調用同步阻塞方法等待綁定操作完成
            sync.channel().closeFuture().sync();//等待服務器鏈路關閉之後main函數才退出
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    class ChildChannelHandler extends 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 TimeServerHandler());
        }
    }

    class TimeServerHandler extends ChannelHandlerAdapter{
        private int counter;
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String)msg;
            System.out.println("The time server received 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);
            System.out.println("done" +currentTime);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            //Netty把write方法並不直接將消息寫入到SocketChannel中,調用write方法只是把待發送的消息放到緩衝數組中,
            // 調用flush方法纔將消息全部寫道SocketChanel
            ctx.flush();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            //釋放相關句柄等資源
            ctx.close();
        }
    }

    public static void main(String[] args) {
        new NettyServer().bind(9988);
    }
}

LineBasedFremeDecoder改造客戶端代碼

public class NetttClient {

    public void connect(String host, int port){
        EventLoopGroup group = new NioEventLoopGroup();
        try{
            //創建客戶端輔助啓動類Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,Boolean.TRUE)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        //創建NioSocketChannel成功之後,進行初始化
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            socketChannel.pipeline().addLast(new StringDecoder());
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            ChannelFuture sync = bootstrap.connect(host, port).sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //釋放NIO線程組資源
            group.shutdownGracefully();
        }
    }

    class TimeServerHandler extends ChannelHandlerAdapter {
        //private final ByteBuf firstMessage;
        private byte[] req;
        private int counter;
        public TimeServerHandler() {
            //給消息添加回車換行符
            req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
        }
        //當客戶端和服務端TCP鏈路建立成功之後,Netty的NI線程會調用channelActive方法,發送查詢指定給服務端
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            //將請求消息發送給服務端
            ByteBuf message;
            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;
            System.out.println("now is :" + body +";the couter is :" + ++counter);

        }

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

    public static void main(String[] args){
        new NetttClient().connect("127.0.0.1",9988);
    }
}

在改造的代碼中,新增了2個解碼器LineBasedFrameDecoder和StringDecoder,發送的帶有回車換行符的消息在被接收後msg就是刪除了回車換行符的消息,不需要再對消息進行編碼解碼。LineBasedFrameDecoder的工作原理就是一次遍歷ByteBUF中可讀字節,判斷看是否有“\n”或者“\r\n”,如有,就以此位置爲結束位置,這樣可以讀到一行一行的息,LineBasedFrameDecoder是以換行符爲結束標誌的解碼器,支持攜帶結束符或者不攜帶結束符兩種解碼方式,同時支持配置單行的最大長度,如果連續讀取到最大長度仍然沒有發現換行符就會拋出異常, 同時忽略之前讀取的異常碼流。

StringDecoder的功能就是將接收到的對象轉成字符串,然後繼續調用Handler,LineBasedFrameDecoder+StringDecoder組合就是按行切換的文本解碼器。

 

 

 

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