目录

1.tcp粘包/拆包原因
2.粘包解决策略
3.具体实现思路
4.netty提供的粘包解决方法

一:tcp粘包/拆包原因

我们都知道Netty是基于NIO的,nio进行客户端与服务端socket编程,在发送消息时,底层是基于TCP传输协议的,首先,TCP协议是基于字节流的,把发送或接受的数据看成一段无结构的字节流,没有边界。其次,在TCP的首部也没有表示数据长度的字段。因此当使用tcp传输数据时,会有粘包和拆包的现象发生。

常见的发生粘包或拆包的原因有以下几种:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

二:粘包解决策略

由于底层即传输层的TCP无法理解应用层的业务数据,所以在传输层无法保证数据包不被拆分和重组的,因为只能通过应用层协议栈的设计上来解决,解决方案可以归纳为:
1、消息定长。例如将每个报文的大小固定为200字节,如果不够,则用空位补空格
2、在包尾增加回车换行符进行分割或者其他分割符进行消息分割,例如FTP协议
3、将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,消息体为实际要发送的数据
4、更复杂的应用层协议

三:实现思路

其实就是具体的实现就是不停的从TCP中读取数据,每次读完数据都要分析是否是一个完整的数据包:
1.如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包
2.如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,够成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。

四:netty提供的粘包解决方法

1)、一个粘包拆包的案例
服务端:

public class TimerServer {

    public static void main(String[] args) throws InterruptedException {
        new TimerServer().start(8090);
    }

    private void start(int port) throws InterruptedException {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap sb = new ServerBootstrap();
            sb.group(bossGroup,workGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new TimeServerHandler());
                        }
                    });
            ChannelFuture cf = sb.bind(port).sync();
            cf.channel().closeFuture().sync();
        }finally {
            // 优雅关闭线程资源
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    private class TimeServerHandler extends ChannelInboundHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            System.out.println("收到客户端发送过来的数据是:" + new String(bytes));
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println(cause.getMessage());
            ctx.close();
        }
    }
}

客户端代码:

public class TimeClient {

    public static void main(String[] args) throws InterruptedException {
        new TimeClient().connect("localhost", 8090);
    }

    private void connect(String localhost, int port) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try{
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new TimeClientHandler());
                        }
                    });
            ChannelFuture cf = b.connect(localhost, port).sync();
            cf.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    private class TimeClientHandler extends ChannelInboundHandlerAdapter{

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            for(int i=1; i<=100; i++){
                byte[] bytes = ("deal tcp test  粘包/拆包现象...............test" +System.getProperty("line.separator")).getBytes();
                ByteBuf buffer = Unpooled.buffer(bytes.length);
                buffer.writeBytes(bytes);
                ctx.writeAndFlush(buffer);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println(cause.getMessage());
            ctx.close();
        }
    }
}

运行发现:服务端收到的消息分为两次,并发生了粘包现象。

2)Netty关于粘包/拆包的解决方法

一:LineBasedFrameDecoder

这个解码器是,通过判断是否有"\n" 或者 “\r\n”, 如果有,则以此位置为结束位置,如果超过定义的最大长度都没找到结束符,则会抛出异常

java netty 粘包处理 netty如何解决粘包_tcp粘包拆包


二:DelimiterBasedFrameDecoder 解码器

此解码器,是通过以自定义的特殊分隔符,进行分割,并定义数据的长度,超过长度未找到分隔符则抛出异常。

服务端改版:

java netty 粘包处理 netty如何解决粘包_java_02


客户端,在给服务端发送数据时,则以 服务端定义的分隔符为数据的结尾即可

客户端改版如下:

java netty 粘包处理 netty如何解决粘包_NIO粘包拆包_03

三:FixedLengthFrameDecoder固定长度解码器

此解码器作用是,按照指定的长度对消息进行自动解码,开发者无需考虑TCP的粘包/拆包问题,所以非常实用。

案例:

客户端给服务端发送的数据长度是:53,因此固定长度,

改造服务端为:

java netty 粘包处理 netty如何解决粘包_java_04

改造客户端为:

java netty 粘包处理 netty如何解决粘包_java netty 粘包处理_05

通过不同环境下,实用LineBasedFrameDecoder ,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder已经可以解决我们日常开发所遇到的大部分问题,当然我们也可以自定义我们自己的解码器进行字节码处理,下一个主要讲解netty关于解码器的实现方面。