本篇文章的详细源码地址:https://github.com/suzhe2018/netty-item,可以下载下来,源码与文章相结合着学习。
1、拆包粘包
改造netty入门里的程序,循环连续发送100条消息。理想情况下服务端应该接受到100条消息,客户端收到100条的响应。
运行服务端,然后在运行客户端,发现服务端只收到了一条消息,客户端也只收到一条响应。
以上问题就是由于tcp粘包导致的,tcp除了粘包还存在拆包问题。
什么是TCP粘包半包?
TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
注意:UCP是基于报文发送的,UDP报文的首部会有16bit来表现UDP数据的长度,所以不同的报文之间是可以区别隔离出来的,所以应用层接收传输层的报文时,不会存在拆包和粘包的问题;
TCP粘包/拆包问题说明
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
解决粘包半包问题:
(1)在包尾增加分割符,比如回车换行符进行分割。
(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度。
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码处理器用于处理粘包/拆包问题。
2、分割符
2.1 DelimiterBasedFrameDecoder
依据入门程序,定义四个类
更改DelimiterEchoServer,添加定长解码处理器
DelimiterEchoServerInHandler 里发送消息,要添加分隔符。
DelimiterEchoClient同样的要添加解码处理器,因为要接收来自服务端的消息。其次在消息发送的时候也要添加分隔符。
运行服务端 ,运行客户端 ,测试。
2.2 LineBasedFrameDecoder
发送消息是给每个消息添加了回车换行符。
使用只需要在服务端和客户端分别添加LineBasedFrameDecoder解码处理器。
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
其次发送消息的时候要加上换行符
3、定长FixedLengthFrameDecoder
FixedLengthFrameDecoder 固定长度解码处理器,它能够按照指定的长度对消息进行自动解码。无论一次接收到多少数据报,它都会按照构造器中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder 会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包。
在服务端和客户端分别添加FixedLengthFrameDecoder,可以根据实际情况设置一个长度。
其次发送消息的时候,不足长度,要用空格补齐
public class FixedUtils {
public static String getBlank(int num){
String blank = "";
for (int i = 0;i<num; i ++){
blank = blank + " ";
}
return blank;
}
}
3、通过LengthFieldBasedFrameDecoder 自定义长度解码处理器
LengthFieldBasedFrameDecoder通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息,只要传入正确的参数,就可以轻松解决“读半包”的问题。
使用只需要在server和client分别加上下面的编解码处理器
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535,
0,2,0,2));
/*给发送出去的消息增加长度字段*/
ch.pipeline().addLast(new LengthFieldPrepender(2));
通过LengthFieldPrepender可以将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段+原消息的方式。
LengthFieldBasedFrameDecoder进行解码,相关参数如下
maxFrameLength:表示的是包的最大长度,
lengthFieldOffset:指的是长度域的偏移量,表示跳过指定个数字节之后的才是长度域;
lengthFieldLength:记录该帧数据长度的字段本身的长度;
lengthAdjustment:长度的一个修正值,可正可负;
initialBytesToStrip:从数据帧中跳过的字节数,表示得到一个完整的数据包之后,忽略多少字节,开始读取实际我要的数据
公式: 实际数据包长度 = 长度域中记录的数据长度 + lengthFieldOffset + lengthFieldLength + lengthAdjustment
解码后,希望丢弃长度域2B字段,所以,只要initialBytesToStrip = 2即可。
1. lengthFieldOffset = 0
2. lengthFieldLength = 2
3. lengthAdjustment = 0 = 数据包长度(14) - lengthFieldOffset - lengthFieldLength - 长度域的值(12)
4. initialBytesToStrip = 2 跳过长度域2B字段开始读取。