Netty-10-案例-粘包拆包

先来看一个简单的例子,服务端向客户端发送1000条数据,然后服务端打印出来代码如下

拆包粘包示例

客户端代码:
image

服务端代码:
image

然后通过channel.pipeline().addLast()把这两个handler添加到对应的端,最后运行看服务端的输出

image

这里存在以下三种类型的输出:

  • 正常的字符串输出
  • 多个字符串 “粘在一起”, 我们定义这种 ByteBuf 为粘包
  • 一个字符串被”拆开”,形成一个破碎的包,我们定义这种 ByteBuf 为半包

为什么会有粘包半包现象

我们在应用层是用的netty,但是操作系统是只认TCP协议的, netty用的ByteBuf来发送数据,到了操作系统还是字节流的,然后数据再到了netty应用层面,再组装成ByteBuf,这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的

所以我们需要在客户端自定义协议来组装数据包,这个过程叫粘包,然后服务端根据这个协议去组装数据包,这个过程叫拆包

拆包原理

在没有netty的情况下,拆包的话就是不断的去从TCP的缓冲区读取数据,每次读取完成之后都需要判断是不是一个完整的包

如果当前读取到的数据不够一个完整的数据包,就保留这些数据,然后继续从缓冲区里面读取

如果当前读取到的加上已经读取的数据,足够拼成一个完整的数据包的话,就将当前读取的数据加上上次读取的数据,拼成一个完整的数据包传给业务层,然后多余的数据保留起来供下次读取用

Netty自带的拆包器

我们自己实现拆包的话,会很麻烦,直接用Netty自带的拆包器就可以了

固定长度的拆包器 FixedLengthFrameDecoder

适用于长度固定的数据包

行拆包器 LineBasedFrameDecoder

每个数据包之间用换行符进行分割话,可以用这个拆包器

分隔符拆包器 DelimiterBasedFrameDecoder

更上面的行拆包器差不多,只不过我们可以自定义分隔符

基于长度域拆包器 LengthFieldBasedFrameDecoder

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

LengthFieldBasedFrameDecoder

重点就是这个基于长度域拆包器

我们之前定义的数据包协议是这样的:
image

关于拆包,我们需要知道以下两点:

  • 长度域在数据包的什么位置,或者说长度域相对整个数据包的偏移量是多少,这里就是4+1+1+1=7
  • 数据包中长度域的长度是多少,这里就是4

有了这两点,就可以构造一个拆包器了

1
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 7, 4);

第一个参数是数据包的最大长度,第二个是长度域的偏移量,第三个是长度域的长度

使用的话就是channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 7, 4));,添加到第一个位置

拒绝非本协议的连接

我们的数据包开头是有一个魔数的,作用就是尽早屏蔽非本协议的客户端,通常在第一个handler处理这个逻辑,下面看一下这个功能具体该如何实现,新建类Sliter,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
}

这里只需要继承一下LengthFieldBasedFrameDecoder,然后写个构造,然后重写decode()方法就可以了

这里的decode()的第二个参数in,每次传递进来都是一个数据包的开头,所以直接用这个in来判断和魔数是不是相等就知道是不是这个协议的连接了

最后把这个类通过ch.pipeline().addLast(new Spliter());加到第一个位置,替换掉上面刚才创建的那个LengthFieldBasedFrameDecoder

这样如果非本协议的数据过来,能尽早判断,关闭连接节省资源

至此.服务端和客户端的pipeline结构是:
image

完整代码已上传gitHub,传送门