Netty-粘包半包:拆包器(编码器和解码器)

26 篇文章 1 订阅
  • 粘包半包

粘包:多个字符串“粘”在了一起,这种 ByteBuf 为粘包

半包:一个字符串被“拆”开,形成一个破碎的包,这种 ByteBuf 为半包

  • 拆包

我们需要知道,尽管我们在应用层面使用了 Netty,但是对于操作系统来说,只认 TCP 协议,尽管我们的应用层是按照 ByteBuf 为 单位来发送数据,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的。因此,我们需要在客户端根据自定义协议来组装我们应用层的数据包,然后在服务端根据我们的应用层的协议来组装数据包,这个过程通常在服务端称为拆包,而在客户端称为粘包。

拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开,举个栗子,发送端将三个数据包粘成两个 TCP 数据包发送到接收端,接收端就需要根据应用协议将两个数据包重新组装成三个数据包。

在没有 Netty 的情况下,用户如果自己需要拆包,基本原理就是不断从 TCP 缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包

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

  • 自定义编码器和解码器

  • 编码器

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class MyLongEncoder extends MessageToByteEncoder<Long> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
        out.writeLong(msg);
    }
}
  • 解码器

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

import java.util.List;

public class MyLongDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if(in.readableBytes()>=4){
            out.add(in.readLong());
        }
    }
}
  • 在客户端和服务端都加入编码器和解码器到pipeline中

  • 客户端

ChannelPipeline pipeline = ch.pipeline();
//1 Long 编码器 解码器
pipeline.addLast(new MyLongEncoder());
pipeline.addLast(new MyLongDecoder());
//2 自定义处理器:通道连接成功后发送一个long
pipeline.addLast(new SimpleChannelInboundHandler<Long>() {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Long x=1234l;
        System.out.println("客户端发送long="+x);
        ctx.channel().writeAndFlush(x);
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("客户端收到long的负数="+(-msg));
    }
});
  • 服务端 

ChannelPipeline pipeline = ch.pipeline();
//1 Long 编码器 解码器
pipeline.addLast(new MyLongEncoder());
pipeline.addLast(new MyLongDecoder());
//2 自定义处理器:通道连接成功后接收long
pipeline.addLast(new SimpleChannelInboundHandler<Long>() {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("服务器收到long="+msg);
        System.out.println("服务器发送long的负数="+(-msg));
        ctx.channel().writeAndFlush(-msg);
    }
});

 解码器同样可以继承自ReplayingDecoder,使用较为方便,但并不是所有的ByteBuf都支持,如果不支持的会抛出异常,同时在某些情况下可能比ByteToMessageDecoder慢。如,网络慢但消息复杂,有可能会把ByteBuf拆分为多个碎片,速度变慢。

示例:

//父类的泛型类Void,代表不需要用户状态
public class MyLongDecoder2 extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //可以节省判断可读字节数长度的逻辑,ReplayingDecoder已经处理过
        out.add(in.readLong());
    }
}

  • Netty自带的解析器

如果我们自己实现拆包,这个过程将会非常麻烦,我们的每一种自定义协议,都需要自己实现,还需要考虑各种异常,而 Netty 自带的一些开箱即用的拆包器已经完全满足我们的需求了。

  • FixedLengthFrameDecoder--固定长度的拆包器/解析器

每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。

  • LineBasedFrameDecoder--行拆包器/解析器

发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。

  • DelimiterBasedFrameDecoder--分隔符拆包器/解析器

DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。

  • LengthFieldBasedFrameDecoder--基于长度域拆包器/解析器

最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包

ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4));

第一个参数指的是数据包的最大长度,第二个参数指的是长度域的偏移量,第三个参数指的是长度域的长度(此例中,二进制数据前4个字节表示数据长度,所以偏移量为第0个字节开始,所占长度为4个字节)

  • HttpObjectDecoder--HTTP协议的数据解析器

一定要注意:服务端和客户端都需要增加拆包器

  • 拒绝非本协议连接

public class Spliter extends LengthFieldBasedFrameDecoder {
    private static final int LENGTH_FIELD_OFFSET = 7;
    private static final int LENGTH_FIELD_LENGTH = 4;

    public Spliter() {
        super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // 屏蔽非本协议的客户端
        if (in.getInt(in.readerIndex()) != PacketCodeC.MAGIC_NUMBER) {
            ctx.channel().close();
            return null;
        }

        return super.decode(ctx, in);
    }
}

使用二进制的魔数在decode方法中尽早屏蔽非本协议的客户端,因为这里的 decode() 方法中,第二个参数 in,每次传递进来的时候,均为一个数据包的开头

服务端和客户端的 pipeline 结构升级图:

总结

  1. 拆包器的作用就是根据我们的自定义协议,把数据拼装成一个个符合我们自定义数据包大小的 ByteBuf,然后送到我们的自定义协议解码器去解码。
  2. Netty 自带的拆包器包括基于固定长度的拆包器,基于换行符和自定义分隔符的拆包器,还有另外一种最重要的基于长度域的拆包器。通常 Netty 自带的拆包器已完全满足我们的需求,无需重复造轮子。
  3. 基于 Netty 自带的拆包器,我们可以在拆包之前判断当前连上来的客户端是否是支持自定义协议的客户端,如果不支持,可尽早关闭,节省资源。 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值