TCP粘包和拆包原理
-
TCP粘包和拆包基本介绍
-
TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务端)都要有——成对的Socket,因此发送端为了将多个发给接收端的包,更有效的发给对象,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。
-
由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。
-
-
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读到的字节数是不确定的,故可能存在以下四种情况
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次就接收到了一个数据包,这个数据包包含了D1和D2的数据,称之为粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2-1包的部分内容,第二次读取到了D2-2包的剩余内容,这称之为拆包
- 服务器分两次读取到了数据包,第一次读取到了D1包的部分内容D1-1,第二次读取到了D1包的剩余内容D1-2和完整的D2包
-
-
TCP粘包和拆包产生的原因
-
数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就是在这个缓冲区上。粘包可以理解为缓冲区的数据堆积,导致多个请求粘在一起,而拆包可以理解为发送的数据大于缓冲区,进行拆分处理
-
-
详细的来说,造成粘包和拆包的主要原因有三个
- 应用程序write写入的字节大小大于套接口发送缓冲大小
- 进行MSS大小的TCP分段
- 以太网帧的payload大于MTU进行IP分片
-
-
粘包和拆包的解决办法
- 由于底层的TCP无法理解上层的业务数据(也就是操作系统是无法解析应用发来的消息的),所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下
- 消息长度固定,累计读取到长度和为定长的LEN的报文后,就认为读取到一个完整的信息
- 将回车换行符作为消息结束符
- 将特殊分隔符作为消息结束符,回车换行符就是一种特殊的结束分隔符
- 通过在消息头中定义长度字段来标识消息的总长度
- 由于底层的TCP无法理解上层的业务数据(也就是操作系统是无法解析应用发来的消息的),所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下
-
Netty中的粘包和拆包解决方案
- 针对上一小节描述的粘包和拆包的解决方案,对于拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty并没有提供相应的组件。对于粘包的问题,由于拆包比较复杂,代码比较处理比较繁琐,Netty提供了4种解码器来解决,分别如下:
- 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小
- 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分
- 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分
- 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度
-
TCP粘包和拆包现象实例
-
TCPPackageClientHandler
-
package com.jl.java.web.tcppackage; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; /** * @author jiangl * @version 1.0 * @date 2021/5/27 21:58 */ public class TCPPackageClientHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //客户端发送10条数据 hello ,server for(int i=0;i<10;i++){ ByteBuf buf = Unpooled.copiedBuffer("hello,server" + i, CharsetUtil.UTF_8); ctx.writeAndFlush(buf); } } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buf = new byte[msg.readableBytes()]; msg.readBytes(buf); System.out.println(new String(buf)); } }
-
-
TCPPackageServerHandler
-
package com.jl.java.web.tcppackage; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; import java.util.UUID; /** * @author jiangl * @version 1.0 * @date 2021/5/27 22:00 */ public class TCPPackageServerHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count =0; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] array = new byte[msg.readableBytes()]; msg.readBytes(array); System.out.println("服务器端接收到数据:"+new String(array)); System.out.println("服务器端接收到消息量=" + (++this.count)); //服务器回送数据给客户端,回送一个随机id ByteBuf responseBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString()+"\n", CharsetUtil.UTF_8); ctx.writeAndFlush(responseBuf); } }
-
-
-
-
TCP粘包和拆包的解决方案
- 使用自定义协议+编解码器来解决
- 关键就是要解决 服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免了TCP粘包、拆包
- 具体实例
- 要求客户端发送5个Message对象,客户端每次发送一个Message对象
- 服务端每次接受一个Message,分5次进行解码,每读取到一个Message,会回复一个Message对象给客户端