- tcp粘包、半包怎么解决的(LineBased和LengthBased,我是用的是LineBased)
- 为什么要使用LineBased,怎么分割的(/r/n,当时没有考虑太多,觉得这个比较简单)
- Netty解决粘包的几种方式
- Netty 拆包粘包的实质
- 项目中如何解决粘包、拆包的问题(基于字符或者基于长度)
- 你这个报文传输的时候会不会遇到报文粘连的情况?如何解决?
- TCP 的粘包的概念是对的吗(面试官:TCP 是面向字节流的,所以这个概念本身是一个伪概念,本身就是可以粘的。但是这种现象还是要解决的)
- 基于Netty实现通信,使用了哪些TCP优化参数?
- 你说网络通信使用的Netty,你都通过那些设置对Netty进行过调优
一、TCP 粘包拆包问题
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 是以字节流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
TCP 中可能出现粘包/拆包的原因:
- Socket 缓冲区与滑动窗口
- MSS/MTU限制
- Nagle算法
1.Socket缓冲区与滑动窗口
应用层在传输数据时,实际上会先将数据写入到 TCP Socket的缓冲区,当缓冲区被写满后,数据才会被写出去。每个TCP Socket 在内核中都有一个发送缓冲区(SO_SNDBUF )和一个接收缓冲区(SO_RCVBUF),TCP 的全双工的工作模式以及 TCP 的滑动窗口便是依赖于这两个独立的 buffer 以及此 buffer 的填充状态。
SO_SNDBUF:进程发送的数据的时候假设调用了一个 send 方法,将数据拷贝进入 Socket 的内核发送缓冲区之中,然后 send 便会在上层返回。
SO_RCVBUF:把接收到的数据缓存入内核,应用进程一直没有调用 read 进行读取的话,此数据会一直缓存在相应 Socket 的接收缓冲区内。
接收缓冲区保存收到的数据一直到应用进程读走为止。对于 TCP,如果应用进程一直没有读取,buffer 满了之后发生的动作是:通知对端 TCP 协议中的窗口关闭。这个便是滑动窗口的实现。
滑动窗口:TCP连接在三次握手的时候,会将自己的窗口大小(window size)发送给对方,其实就是 SO_RCVBUF 指定的值。
- 发送方:之后在发送数据的时,发送方必须要先确认接收方的窗口没有被填充满,如果没有填满,则可以发送。每次发送数据后,发送方将自己维护的对方的 window size 减小,表示对方的 SO_RCVBUF 可用空间变小。
- 接收方:当接收方处理开始处理 SO_RCVBUF 中的数据时,会将数据从 Socket 在内核中的接受缓冲区读出,此时接收方的 SO_RCVBUF 可用空间变大,即 window size 变大,接受方会以 ack 消息的方式将自己最新的 window size 返回给发送方,此时发送方将自己的维护的接受方的 window size 设置为ack消息返回的 window size。
此外,发送方可以连续的给接受方发送消息,只要保证对方的 SO_RCVBUF 空间可以缓存数据即可,即 window size>0。当接收方的 SO_RCVBUF 被填充满时,此时 window size=0,发送方不能再继续发送数据,要等待接收方 ack 消息,以获得最新可用的 window size。
SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K;
2.MSS/MTU分片
MTU (Maxitum Transmission Unit,最大传输单元) 是链路层对一次可以发送的最大数据的限制。MSS (Maxitum Segment Size,最大分段大小) 是 TCP 报文中 data 部分的最大长度,是传输层对一次可以发送的最大数据的限制。
数据在传输过程中,每经过一层,都会加上一些额外的信息:
- 应用层:只关心发送的数据 data,将数据写入 Socket 在内核中的缓冲区 SO_SNDBUF 即返回,操作系统会将 SO_SNDBUF 中的数据取出来进行发送;
- 传输层:会在 data 前面加上 TCP Header(20字节);
- 网络层:会在 TCP 报文的基础上再添加一个 IP Header,也就是将自己的网络地址加入到报文中。IPv4 中 IP Header 长度是 20 字节,IPV6 中 IP Header 长度是 40 字节;
- 链路层:加上 Datalink Header 和 CRC。会将 SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和 Type 域加入。SMAC+DMAC+Type+CRC 总长度为 18 字节;
- 物理层:进行传输。
3.Nagle 算法
Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle 算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓 “小段”,指的是小于 MSS 尺寸的数据块;所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的 ACK 确认该数据已收到。
Nagle 算法的规则:
- 如果 SO_SNDBUF 中的数据长度达到 MSS,则允许发送;
- 如果该 SO_SNDBUF 中含有 FIN,表示请求关闭连接,则先将 SO_SNDBUF 中的剩余数据发送,再关闭;
- 设置了 TCP_NODELAY=true 选项,则允许发送。TCP_NODELAY 是取消 TCP 的确认延迟机制,相当于禁用了 Negle 算法。正常情况下,当 Server 端收到数据之后,它并不会马上向 client 端发送 ACK,而是会将 ACK 的发送延迟一段时间(一般是 40ms),它希望在 t 时间内 server 端会向 client 端发送应答数据,这样 ACK 就能够和应答数据一起发送,就像是应答数据捎带着 ACK 过去。当然,TCP 确认延迟 40ms 并不是一直不变的, TCP 连接的延迟确认时间一般初始化为最小值 40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。另外可以通过设置 TCP_QUICKACK 选项来取消确认延迟;
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
基于以上问题,TCP层肯定是会出现当次接收到的数据是不完整数据的情况。
出现粘包(针对后面的数据包而言)可能的原因有:
- 发送方每次写入数据 < Socket缓冲区大小;
- 接收方读取Socket缓冲区数据不够及时。
出现半包(针对前面的数据包而言)的可能原因有:
- 发送方每次写入数据 > Socket缓冲区大小;
- 发送的数据大于协议 MTU,所以必须要拆包。
解决问题肯定不是在4层来做而是在应用层,通过定义通信协议来解决粘包和拆包的问题。
发送方 和 接收方约定某个规则:
- 当发生粘包的时候通过某种约定来拆包;
- 如果在拆包,通过某种约定来将数据组成一个完整的包处理。
二、Netty 解决办法
1. 定长协议
指定一个报文具有固定长度。比如约定一个报文的长度是 5 字节,那么:
报文:1234,只有4字节,但是还差一个怎么办呢,不足部分用空格补齐。就变为:1234 。
如果不补齐空格,那么就会读到下一个报文的字节来填充上一个报文直到补齐为止,这样粘包了。
定长协议的优点是使用简单,缺点很明显:浪费带宽。
Netty 中提供了 FixedLengthFrameDecoder ,支持把固定的长度的字节数当做一个完整的消息进行解码。
2. 特殊字符分割协议
很好理解,在每一个你认为是一个完整的包的尾部添加指定的特殊字符,比如:\n,\r等等。
需要注意的是:约定的特殊字符要保证唯一性,不能出现在报文的正文中,否则就将正文一分为二了。
Netty 中提供了 DelimiterBasedFrameDecoder 根据特殊字符进行解码,LineBasedFrameDecoder默认以换行符作为分隔符。
3. 变长协议
变长协议的核心就是:将消息分为消息头和消息体,消息头中标识当前完整的消息体长度。
发送方在发送数据之前先获取数据的二进制字节大小,然后在消息体前面添加消息大小;
接收方在解析消息时先获取消息大小,之后必须读到该大小的字节数才认为是完整的消息。
Netty 中提供了 LengthFieldBasedFrameDecoder ,通过LengthFieldPrepender 来给实际的消息体添加 length 字段。
总结 Netty 自带解决方式:
消息定长:FixedLengthFrameDecoder 类
包尾增加特殊字符分割:
- 行分隔符类:LineBasedFrameDecoder
- 自定义分隔符类 :DelimiterBasedFrameDecoder
将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
在Netty中,通过使用合适的解码器(如定长解码器、换行符解码器、分隔符解码器和长度字段解码器)可以有效解决粘包和拆包问题。同时,结合TCP的重传机制和应用层的重传策略,可以有效地减少丢包现象,确保数据传输的可靠性。
合理设置 TCP 参数在某些场景下对于性能的提升可以起到显著的效果。
三、参考
Netty 中的粘包和拆包详解_netty拆包粘包-CSDN博客