述
先来看一个简单的例子,服务端向客户端发送1000条数据,然后服务端打印出来代码如下
拆包粘包示例
客户端代码:
服务端代码:
然后通过channel.pipeline().addLast()
把这两个handler添加到对应的端,最后运行看服务端的输出
这里存在以下三种类型的输出:
- 正常的字符串输出
- 多个字符串 “粘在一起”, 我们定义这种 ByteBuf 为粘包
- 一个字符串被”拆开”,形成一个破碎的包,我们定义这种 ByteBuf 为半包
为什么会有粘包半包现象
我们在应用层是用的netty,但是操作系统是只认TCP协议的, netty用的ByteBuf来发送数据,到了操作系统还是字节流的,然后数据再到了netty应用层面,再组装成ByteBuf,这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的
所以我们需要在客户端自定义协议来组装数据包,这个过程叫粘包,然后服务端根据这个协议去组装数据包,这个过程叫拆包
拆包原理
在没有netty的情况下,拆包的话就是不断的去从TCP的缓冲区读取数据,每次读取完成之后都需要判断是不是一个完整的包
如果当前读取到的数据不够一个完整的数据包,就保留这些数据,然后继续从缓冲区里面读取
如果当前读取到的加上已经读取的数据,足够拼成一个完整的数据包的话,就将当前读取的数据加上上次读取的数据,拼成一个完整的数据包传给业务层,然后多余的数据保留起来供下次读取用
Netty自带的拆包器
我们自己实现拆包的话,会很麻烦,直接用Netty自带的拆包器就可以了
固定长度的拆包器 FixedLengthFrameDecoder
适用于长度固定的数据包
行拆包器 LineBasedFrameDecoder
每个数据包之间用换行符进行分割话,可以用这个拆包器
分隔符拆包器 DelimiterBasedFrameDecoder
更上面的行拆包器差不多,只不过我们可以自定义分隔符
基于长度域拆包器 LengthFieldBasedFrameDecoder
最通用的一种拆包器,只要你的自定义协议中包含长度域的字段,就可以使用这个拆包器
LengthFieldBasedFrameDecoder
重点就是这个基于长度域拆包器
我们之前定义的数据包协议是这样的:
关于拆包,我们需要知道以下两点:
- 长度域在数据包的什么位置,或者说长度域相对整个数据包的偏移量是多少,这里就是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
27public 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结构是:
完整代码已上传gitHub,传送门