谈到TCP粘包/半包的解决方案,我们不妨先认识造成TCP粘包/半包的原因有哪些,以便于更深刻理解解决方案的原理
一.TCP粘包
1.现象:发送:abc,def,接收:abcdef 2.原因:
- 应用层:接受数据的Bytebuf缓冲区设置太大(Netty默认为1024)存储了多个TCP包,一次性取的时候取出缓冲区所有数据,导致多个包粘在一起.
- 滑动窗口:发送方发送消息太快,接收方接收消息也快,但是处理消息太慢,并且接收窗口足够大,那么此时消息就会堆积在接收窗口,那么就会导致多个包粘在一起
- Nagle算法:Nagle算法是操作系统底层自动实现的,为了减少发送次数,提高效率,操作系统会在缓冲区未满的时候等待消息填充,直到缓冲区满的时候一并发送,造成粘包
二.TCP半包
1.现象:发送:abcdef,接收:abc,def 2.原因:
- 应用层:接受数据的Bytebuf缓冲区设置太小(Netty默认为1024)存储不了一个完整的TCP包,一次性只能取出TCP包的部分数据
- 滑动窗口:发送方发送窗口较大,接收方接收窗口较小,导致接收方只能接收TCP包的部分数据,导致TCP半包
- MSS限制:为了限制TCP包的大小,会将大的TCP包分组成多个小的TCP包发送,导致半包问题
三.TCP粘包/半包解决方案
1.FixedLengthFrameDecoder定长解析器
向pipeline中加入FixedLengthFrameDecoder(int length)定长解析器,取出指定字节长度的数据作为完成数据,不过这要求完整数据的长度是定长
使用方法,请尝试理解下面源码
public class FixedLengthFrameDecoderTest {
public static ByteBuf Message(){
StringBuilder sb = new StringBuilder();
char c = 'a';
for(int i=0;i<20;i++){
sb.append(c);
c++;
}
return Unpooled.copiedBuffer(sb.toString(),StandardCharsets.UTF_8);
}
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new FixedLengthFrameDecoder(10),
new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf buf) throws Exception {
int len = buf.readableBytes();
byte[] msg = new byte[len];
buf.readBytes(msg);
System.out.println(new String(msg,0,len));
}
}
);
channel.writeInbound(Message());
}
}
2.LineBasedFrameDecoder行解析器
向pipeline中加入LineBasedFrameDecoder行解析器,会以换行符"\n"作为完整消息边界,不过着要求每一个完整消息的末尾都加上换行符作为
使用方法,请尝试理解下面源码
public class LineBasedFrameDecoderTes {
public static ByteBuf Massage(){
StringBuilder msg = new StringBuilder();
char c = 'a';
for(int i=0;i<10;i++){
msg.append(c);
c++;
}
msg.append("\n");
for(int i=0;i<10;i++){
msg.append(c);
c++;
}
msg.append("\n");
return Unpooled.copiedBuffer(msg.toString(), StandardCharsets.UTF_8);
}
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LineBasedFrameDecoder(10),
new SimpleChannelInboundHandler<ByteBuf>(){
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf buf) throws Exception {
int len = buf.readableBytes();
byte[] msg = new byte[len];
buf.readBytes(msg);
System.out.println(new String(msg,0,len));
}
}
);
channel.writeInbound(Massage());
}
}
3.LengthFieldBasedFrameDecoder
向pipeline中加入LineBasedFrameDecoder行解析器,以设置长度读取消息 ,不过这要求数据以消息长度+消息体的形式发送,也即先告知服务器数据的长度,再发送完整的数据
参数设置:
1.maxFrameLength:消息最大字节长度
2.lengthFieldOffset:声明消息长度变量的偏移量(也即字节下标是多少)
3.lengthFieldLength:声明消息长度变量的字节大小(一般用int声明,4字节)
4.lengthAdjustment:调着消息体的其实位置,如果忽略一个字节就设置为1,不忽略设置为0
5.initialBytesToStrip:最终消息从头剥离几个字节
使用方法,请尝试理解下面源码
public class LengFiledBasedFrameDecoderTest {
public static ByteBuf Message(){
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
StringBuilder sb = new StringBuilder();
char c = 'a';
for(int i=0;i<20;i++){
sb.append(c);
c++;
}
byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8);
buf.writeInt(bytes.length);
buf.writeBytes(bytes);
return buf;
}
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LengthFieldBasedFrameDecoder(1024,0,4,0,4),
new SimpleChannelInboundHandler<ByteBuf>(){
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf buf) throws Exception {
int len = buf.readableBytes();
byte[] msg = new byte[len];
buf.readBytes(msg);
System.out.println(new String(msg,0,len));
}
}
);
channel.writeInbound(Message());
}
}
|