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{
        @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() {
                        //创建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组合就是按行切换的文本解码器。

当然,基于LineBasedFrameDecoder+StringDecoder组合是针对回车换行符,如果消息没有回车换行符的消息就需要使用其他的半包解码器,Netty提供了支持多种TCP粘包/拆包的解码器,用来满足不同需求

 

 

 

你可能感兴趣的:(netty,netty)