NIO基于长度域的报文在Netty下的解码

1, 先复习一下粘包/拆包

1.1, 粘包/拆包的含义

TCP是个“流”协议, 并不了解上层业务数据的具体含义, 它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

假设客户端分别发送了两个数据包D1和D2给服务端,可能存在以下4种情况:

(a)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

(b)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

(c)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

(d)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

1.2, 粘包/拆包的处理

粘包/拆包的解决方法都是在报文结构上做处理,一般有3种方式: 定长报文、报文分隔符 、报文长度域

在NIO下, 框架Netty对于以上3种方式分别有自己的实现: FixedLengthFrameDecoder、DelimiterBasedFrameDecoder 、LengthFieldBasedFrameDecoder

 

2, 基于长度域的报文在Netty下的解码

对于“长度域”的值, 虽然底层都是以字节的形式传输, 但是在上层数据类型上, 长度域有‘字符串’和‘数字’类型两种.

假设原报文内容是

x=1111,y=2222,z=3333 

原报文20个字节, 在对其添加长度域时, Java开发者可能见过下面这种结构, 尤其是在金融/银行开发中

00000020x=1111,y=2222,z=3333

上面报文的长度域就是‘字符串’类型, 对应的整型值为原报文的字节长度, 不足8位左边补0.

对于‘字符串’类型的长度域, 发送方输出流的方式如下:

out.write("00000020".getBytes());//字符串00000020转化为字节
out.write("x=1111,y=2222,z=3333".getBytes());

//后面再说NIO下我们该如何去解码字符串长度域的报文. 

接着说NIO下Netty自带的长度域解码器LengthFieldBasedFrameDecoder, 它支持的长度域就是‘数字’类型. 

对于‘数字’类型的长度域, 如果约定长度为4, 则其报文结构大抵如下, 不太好刻画, 前面4位是数字20转化的字节.

[0,0,0,20]x=1111,y=2222,z=3333

发送方输出流的方式如下:

out.write(new byte[]{0,0,0,20});//数字20转换为4位字节数组
out.write("x=1111,y=2222,z=3333".getBytes());

然后Netty接收端直接使用LengthFieldBasedFrameDecoder就可以很方便解码

new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)

如果客户端和服务端都是Netty开发, 大家默认的就是‘数字’类型的长度域, 发送端直接使用Netty自带的LengthFieldPrepender编码器就行了.

但是如果你作为数据接收方的NIO开发者, 而发送方是权威方, 它给的报文的长度域是‘字符串’类型时, 你该怎么处理?

这个时候, 我们可以基于Netty自定义一个解码器, 专门处理字符串类型的长度域, 实现代码如下:

package com.test;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.CharsetUtil;

import java.util.List;

/**
 * 字符串类型的长度域报文解码器
 *
 * @Author:tt
 * @Description:
 * @CreateTime:2019/6/26 下午11:30
 */
public class StringLengthFieldDecoder extends ByteToMessageDecoder {

    //长度域的字符串长度,比如:长度字段的长度为8,则报文有100个字节时,长度域值为:00000100
    private int lengthFieldSize;

    public StringLengthFieldDecoder(int lengthFieldSize) {
        this.lengthFieldSize = lengthFieldSize;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = this.decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {

        if (in.readableBytes() < lengthFieldSize) {
            return null;
        }

        //长度域的字符串值
        String lengthFieldValStr = in.readBytes(lengthFieldSize).toString(CharsetUtil.UTF_8);

        //原报文长度
        int frameLength = Integer.parseInt(lengthFieldValStr);

        if (in.readableBytes() < frameLength) {
            //这一步很重要,回退读索引
            in.readerIndex(in.readerIndex() - lengthFieldSize);
            return null;
        }

        return in.readBytes(frameLength);
    }
}

转载于:https://my.oschina.net/wangxu3655/blog/3066594

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值