美文网首页
netty 数据包黏包拆包处理器使用及遇到的问题

netty 数据包黏包拆包处理器使用及遇到的问题

作者: 真老根儿 | 来源:发表于2018-07-23 20:30 被阅读0次

    netty 数据包黏包拆包处理器使用及遇到的问题

    最近因为在做一个游戏后端,需要用到netty,在与前端沟通之后规定了数据包结构:
    | tag | encode | encrypt | command | length | body |

    结构 类型 解释
    tag byte 标签,默认值为0x01
    encode byte 编码格式,默认值为0x01
    encrypt byte 加密类型,默认值为0x01
    command int 指令,根据指令去解析body
    length int 长度,body内容的长度
    body string 内容,json序列化之后的对象

    刚开始使用继承ByteToMessageDecoderMessageToByteEncoder做拆包黏包处理。
    ByteToMessageDecoder 抽象方法实现

    public static final byte PACKAGE_TAG = 0x01;
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception {
            buf.markReaderIndex();
            byte tag = buf.readByte();
            if (tag != PACKAGE_TAG) {
                throw new CorruptedFrameException("标志错误");
            }
            byte encode = buf.readByte();
            byte encrypt = buf.readByte();
            int command = buf.readInt();
            int length = buf.readInt();
            byte[] data = new byte[length];
            buf.readBytes(data);
            Message message = new Message(tag, encode, encrypt, command, length,
             new String(data, "UTF-8"));
            out.add(message);
        }
    

    MessageToByteEncoder 抽象方法实现

    @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
            out.writeByte(MessageDecoder.PACKAGE_TAG);
            out.writeByte(msg.getEncode());
            out.writeByte(msg.getEncrypt());
            out.writeInt(msg.getCommand());
            byte[] bytes = msg.getBody().getBytes("UTF-8");
            out.writeInt(bytes.length);
            out.writeBytes(bytes);
        }
    

    Message.class

    public class Message {
    
        private byte tag;
        /*  编码*/
        private byte encode;
        /*加密*/
        private byte encrypt;
        /* 类型**/
        private int command;
        /*包的长度*/
        private int length;
        /*内容*/
        private String body;
    }
    

    这样在刚开始的工作中数据包传输没有问题,不过数据包的大小超过512b的时候就会抛出异常了。

    io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException:
    readerIndex(11) + length(565) exceeds writerIndex(512): PooledUnsafeDirectByteBuf(ridx: 11, widx: 512, cap: 512)

    数据包的长度为565,而ByteToMessageDecoder只处理到了512。我并没有找到控制ByteToMessageDecoder最大读写的方法。
    但是,因为解码器继承ChannelInboundHandlerAdapter类,而我们可以使用多个处理器一起处理数据

    解决办法

    配合解码器DelimiterBasedFrameDecoder一起使用,在数据包的末尾使用换行符\n表示本次数据包已经结束,当DelimiterBasedFrameDecoder把数据切割之后,再使用ByteToMessageDecoder实现decode方法把数据流转换为Message对象。

    我们在ChannelPipeline加入DelimiterBasedFrameDecoder解码器

    public class ServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) {
            ChannelPipeline pipeline = ch.pipeline();
            //使用\n作为分隔符
            pipeline.addLast(new LoggingHandler(LogLevel.INFO));
            pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
            pipeline.addLast(new MessageEncoder());
            pipeline.addLast(new MessageDecoder());
            pipeline.addLast(new MessageHandler());
        }
    }
    

    MessageToByteEncoder的实现方法encode()增加out.writeBytes(new byte[]{'\n'});

     @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
            out.writeByte(MessageDecoder.PACKAGE_TAG);
            out.writeByte(msg.getEncode());
            out.writeByte(msg.getEncrypt());
            out.writeInt(msg.getCommand());
            byte[] bytes = msg.getBody().getBytes("UTF-8");
            out.writeInt(bytes.length);
            out.writeBytes(bytes);
            //在写出字节流的末尾增加\n表示数据结束
            out.writeBytes(new byte[]{'\n'});
        }
    

    这时候就可以愉快的继续处理数据了。
    等我还没有高兴半天的时候,问题又来了。

    io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(11) + length(379) exceeds writerIndex(276): PooledUnsafeDirectByteBuf(ridx: 11, widx: 276, cap: 276)

    等等等,,,怎么又报错了,不是已经加了黏包处理了吗??,解决问题把,首先看解析的数据包结构

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 01 01 00 00 00 06 00 00 01 0a 7b 22 69 64 22 |...........{"id"|
    |00000010| 3a 33 2c 22 75 73 65 72 6e 61 6d 65 22 3a 22 31 |:3,"username":"1|
    |00000020| 38 35 30 30 33 34 30 31 36 39 22 2c 22 6e 69 63 |8500340169","nic|
    |00000030| 6b 6e 61 6d 65 22 3a 22 e4 bb 96 e5 9b 9b e5 a4 |kname":"........|
    |00000040| a7 e7 88 b7 22 2c 22 72 6f 6f 6d 49 64 22 3a 31 |....","roomId":1|
    |00000050| 35 32 37 32 33 38 35 36 39 34 37 34 2c 22 74 65 |527238569474,"te|
    |00000060| 61 6d 4e 61 6d 65 22 3a 22 e4 bf 84 e7 bd 97 e6 |amName":".......|
    |00000070| 96 af 22 2c 22 75 6e 69 74 73 22 3a 7b 22 75 6e |..","units":{"un|
    |00000080| 69 74 31 22 3a 7b 22 78 22 3a 31 30 2e 30 2c 22 |it1":{"x":10.0,"|
    |00000090| 79 22 3a 31 30 2e 30 7d 2c 22 75 6e 69 74 32 22 |y":10.0},"unit2"|
    |000000a0| 3a 7b 22 78 22 3a 31 30 2e 30 2c 22 79 22 3a 31 |:{"x":10.0,"y":1|
    |000000b0| 30 2e 30 7d 2c 22 75 6e 69 74 33 22 3a 7b 22 78 |0.0},"unit3":{"x|
    |000000c0| 22 3a 31 30 2e 30 2c 22 79 22 3a 31 30 2e 30 7d |":10.0,"y":10.0}|
    |000000d0| 2c 22 75 6e 69 74 34 22 3a 7b 22 78 22 3a 31 30 |,"unit4":{"x":10|
    |000000e0| 2e 30 2c 22 79 22 3a 31 30 2e 30 7d 2c 22 75 6e |.0,"y":10.0},"un|
    |000000f0| 69 74 35 22 3a 7b 22 78 22 3a 31 30 2e 30 2c 22 |it5":{"x":10.0,"|
    |00000100| 79 22 3a 31 30 2e 30 7d 7d 2c 22 73 74 61 74 75 |y":10.0}},"statu|
    |00000110| 73 22 3a 31 7d 0a                               |s":1}.          |
    +--------+-------------------------------------------------+----------------+
    

    接收到的数据是完整的没错,但是还是报错了,而且数据结尾的字节的确是0a,转化成字符就是\n没有问题啊。

    ByteToMessageDecoderdecode方法里打印ByteBuf buf的长度之后,问题找到了

    长度 : 10

    这就是说在进入到ByteToMessageDecoder这个解码器的时候,数据包已经只剩下10个长度了,那么长的数据被上个解码器DelimiterBasedFrameDecoder隔空劈开了- -。问题出现在哪呢,看上面那块字节流的字节,找到第11个字节,是0a。。。。因为不是标准的json格式,最前面使用了3个字节 加上2个int长度的属性,所以 数据包头应该是11个字节长。
    DelimiterBasedFrameDecoder在读到第11个字节的时候读成了\n,自然而然的就认为这个数据包已经结束了,而数据进入到ByteToMessageDecoder的时候就会因为规定的body长度不等于length长度而出现问题。

    再次解决问题

    思来想去 不实用\n 这样的单字节作为换行符,很容易在数据流中遇到,转而使用\r\n俩字节来处理,而这俩字节出现在前面两个int长度中的几率应该很小。

    看最后的代码

    public class ServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) {
            ChannelPipeline pipeline = ch.pipeline();
            //这里使用自定义分隔符
            ByteBuf delimiter = Unpooled.copiedBuffer("\r\n".getBytes());
            pipeline.addFirst(new DelimiterBasedFrameDecoder(8192, delimiter));
            pipeline.addLast(new MessageEncoder());
            pipeline.addLast(new MessageDecoder());
            pipeline.addLast(new MessageHandler());
        }
    }
    
    
    public class MessageEncoder extends MessageToByteEncoder<Message> {
        @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
            out.writeByte(MessageDecoder.PACKAGE_TAG);
            out.writeByte(msg.getEncode());
            out.writeByte(msg.getEncrypt());
            out.writeInt(msg.getCommand());
            byte[] bytes = msg.getBody().getBytes("UTF-8");
            out.writeInt(bytes.length);
            out.writeBytes(bytes);
            //这里最后修改使用\r\n
            out.writeBytes(new byte[]{'\r','\n'});
        }
    }
    
    

    再次运行程序 数据包可以正常接收了。

    总结

    • 以前使用netty的时候也仅限于和硬件交互,而当时的硬件受限于成本问题是一条一条处理数据包的,所以基本上不会考虑黏包问题
    • 然后就是ByteToMessageDecoderMessageToByteEncoder两个类是比较底层实现数据流处理的,并没有带有拆包黏包的处理机制,需要自己在数据包头规定包的长度,而且无法处理过大的数据包,因为我一开始首先使用了这种方式处理数据,所以后来就没有再换成DelimiterBasedFrameDecoderStringDecoder来解析数据包,最后使用json直接转化为对象。

    相关文章

      网友评论

          本文标题:netty 数据包黏包拆包处理器使用及遇到的问题

          本文链接:https://www.haomeiwen.com/subject/qkqsmftx.html