Netty——粘包与拆包

小龙 765 2019-11-06

粘包与拆包分析

在进行网络数据传输的时候,所有的传输操作都是有数据的长度限制的,所以在进行批量数据传输的过程之中,如果没有进行适当的处理,那么就会造成传输数据的不准确。
在服务器端上所有可以接收到数据包都是有长度限制的,而在使用Netty开发框架进行网络通讯程序编写的时候,这些数据包的接收与控制实际上全部由Netty包装了,所有开发者在很多的情况下根本感觉不到这种长度的限制,但是一旦说有了长度的限制,那么对于数据的传输就会比较麻烦了;可以考虑几种情况:

情况一: 现在服务器端和客户端已经交互完毕了,服务器端可以接收10个字节的数据内容,那么此时客户端也应该考虑只发送10个字节的数据内容,而要想达到这种理想的 情况,在程序代码的设计之中就始终要保持数据的长度,如果数据长度不够的时候需要补充一些空位,例如:
qingkuangyi.png
但是所有的服务器都有各自的带宽限制,如果现在带宽被无用的浪费掉,那么所带来的问题就是你的传输效率一定会有所下降,所有理论上最好的做法就是只传递真实的有效的处理数据

情况二: 在进行数据传输的时候有的客户端可能发送的数据较长,有的客户端发送的数据较短,但是对于服务器端应该考虑不管长还是短,都要按照完整的数据进行传输,所以这个时候,可以考虑为数据包追加一个长度的头部信息
qingkuanger.png

情况三: 设置一个占位分隔符区分不同的数据内容,不同的数据内容之间使用一些特定的符号作为区分,而后服务器端直接依赖于此信息进行数据的读取
qingkuangsan.png

但是对应此时的数据传输需要注意一个问题,对于服务器端来讲并不能无限的进行客户端的接收,他每一次接收的数据都是有一个最大的长度限制的,如果有了长度的限制,那么就会出现如下的问题:
qingkuangsi.png

如果直接使用原生的TCP协议进行功能的实现,那么这样的困难度是相当高的,因为需要不断的面对于各种数据传输进行处理操作,同时有需要针对不断提升的需求进行改进。在使用了Netty之后这些问题都可以得到解决,在Netty里面提供有系统默认的粘包和拆包处理。看一下如果在没有任何粘包和拆包的处理情况下,Netty对数据的接收和发送是什么样的

    //客户端发送500条数据	    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
	
        for (int x = 0 ; x < 500 ; x++){
            ctx.writeAndFlush("{"+x+"}"+"好好学习吧!!");
        }
    }
   //服务器端接收并返回所有数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            //获取输入的内容
            String inputMessage = (String) msg;
            System.err.println("{EchoServer端}接收到消息内容:" + inputMessage);
            //设置回应消息
            String echoConect = "【ECHO】" + inputMessage;
            ctx.writeAndFlush(echoConect);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //执行完毕之后,有无异常都抛弃接收到的消息
            ReferenceCountUtil.release(msg);
        }
    }

执行结果
服务器端
fuwuqiduanjieguo.png
客户端
kehuduanjieguo.png

粘包与拆包处理

原本需要进行500次的数据交互,但最终发现只执行了几次,原因是在默认的情况下Netty都有自己的数据接收长度,如果在没有进行任何的配置时,当数据长度达到了可以接收长度的范围时就表示当前的信息已经可以进行处理了,所有最终的结果个预期的结果不一样。

要想在Netty中实现粘包和拆包处理基本的实现思路就在于需要通过分隔符区分不同的传输内容,所有在整个的实现过程之中,要进行正确的传输就需要在每一个数据之后设置相应的公共分隔符。
在java里面有一个基本环境属性:line.separator。该环境属性是一个换行,在Netty中使用的就是JDK提供的

System.getProperties().getProperty("line.separator")

作为默认的分隔符。只要在有java运行的机制里面都会存在同样的分隔符,所有使用这个分隔符进行数据分割定义是最标准的。

在要发送的数据后面加上分隔符
客户端配置

   //客户端发送500条数据
   @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int x = 0 ; x < REPEAT ; x++){
           ctx.writeAndFlush("{"+x+"}"+"好好学习吧!!"+System.getProperties().getProperty("line.separator"));
        }
    }

服务器端配置

    //服务器端返回接收到的所有消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            //获取输入的内容
            String inputMessage = (String) msg;
            System.err.println("{EchoServer端}接收到消息内容:" + inputMessage);
            String echoConect = "【ECHO】" + inputMessage + System.getProperties().getProperty("line.separator");
            ctx.writeAndFlush(echoConect);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //执行完毕之后,有无异常都抛弃接收到的消息
            ReferenceCountUtil.release(msg);
        }
    }

要想配置的分隔符生效,不需要在处理链中加上相应的操作处理,这两个设置使用其中任意一个都可以,推荐使用LineBasedFrameDecoder,而且客户端与服务器端配置是一致的

FixedLengthFramedDecoder 设置定长的数据内容,所以长度不够的数据会自动补充空位

 ch.pipeline().addLast(new FixedLengthFrameDecoder(50));

LineBasedFrameDecoder 设置每行传输的最大长度,在每一行中可能有多个数据包

ch.pipeline().addLast(new LineBasedFrameDecoder(100));
cliectBootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LineBasedFrameDecoder(100));
                    ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new EchoClientHandlerBigDataTest());
                }
            });

配置了LineBasedFrameDecoder,这样在进行数据接收的时候就会自动的排查里面是否有指定换行符,会自动进行数据的截取,并且每一次截取的结果都会直接通过channelRead()方法读取
运行结果
服务器端
服务器端结果2.png
客户端
客户端结果2.png

自定义分隔符

上面使用的JDK默认提供的分割符,该分隔符是由系统变量直接定义完成的,但是在实际的开发过程中如果使用换行作为默认分割有可能在进行数据处理的时候不方便,所以提供了自定义分割符,由开发者自己去定义。

使用自定义分隔符,就需要使用DelimiterBasedFrameDecoder解码器,该解码器与“LineBasedFrameDecoder”的基本功能是相同的,唯一不同的是“DelimiterBasedFrameDecoder”使用的是自定义分隔符,“LineBasedFrameDecoder”使用的是系统默认的分隔符

客户端配置

    //客户端发送500条数据
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int x = 0 ; x < REPEAT ; x++){
            ctx.writeAndFlush("{"+x+"}"+"好好学习吧!!"+ "%$%");
        }
    }

cliectBootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(100, Unpooled.copiedBuffer("%$%".getBytes())));
                    ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new EchoClientHandlerBigDataTest());
                }
            });

服务器端配置

    //服务器端返回接收到的所有消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            //获取输入的内容
            String inputMessage = (String) msg;
            System.err.println("{EchoServer端}接收到消息内容:" + inputMessage);
            String echoConect = "【ECHO】" + inputMessage + "%$%";
            ctx.writeAndFlush(echoConect);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //执行完毕之后,有无异常都抛弃接收到的消息
            ReferenceCountUtil.release(msg);
        }
    }
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(100, Unpooled.copiedBuffer("%$%".getBytes())));
                    ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new EchoServerHandlerBigDataTest());
                }
            });

# Netty # 粘包与拆包