TCP-缓冲区和粘包、拆包有啥关系?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP-缓冲区和粘包、拆包有啥关系?相关的知识,希望对你有一定的参考价值。

参考技术A     你了解TCP缓冲区吗?它和TCP传输中的粘包和拆包有什么关系呢?粘包和拆包分别发生在TCP的那个阶段呢?

    先简单回顾下TCP概念:在网络传输中TCP是面向连接的、可靠的、双通道、字节流一对一传输。TCP双方通信必须要先建立连接,然后分配必要的内核资源。双方交换完毕数据之后必须都要断开连接用来释放系统资源,长链接可以不必断开连接复用同一个通道。那么什么是TCP的缓冲区呢?

    操作系统中有两个空间:用户空间和内核空间。每一个socket连接都是在内核空间,内核针对每一个socket都有一个发送缓冲区和接收缓冲区。TCP的双工工作模式以及流量控制就是依赖这两个缓冲区的填充来实现的。

    我们之前用socket获取“OutputStream”获取一个输出流进行字节的写出,其实是写入到了“send buffer”发送缓冲区中,这个时候数据不一定会发送到对方机器上。“write()”方法仅仅是将用户空间数据拷贝到了内核发送缓冲区中,具体什么时候发送由TCP决定。

    TCP会从发送缓冲区中把数据通过网卡发送到目标机器的内核缓冲区中。如果系统一直没有调用"recv()"方法进行读取的话,那么数据将会一直挤压在socket的recv buffer中。

    TCP 粘包、拆包问题的由来:

    如果你看懂了上面这幅图的话,那么对于粘包和拆包的问题就比较好理解了。在这里我想先问一个问题,粘包和拆包是发生在传输过程中吗?

    粘包和拆包问题究竟发生在什么阶段?首先我们需要清楚地了解TCP数据是可靠的,因此肯定不是传输的过程中!因为数据发送是从缓冲区->网卡,因此粘包问题是从缓冲区读取数据的时候发生的。拆包则是从缓冲区到网卡的阶段发生的。

    这里先解释下粘包:所谓的粘包就是发送方在同一时刻发出了两个或者两个以上的包到接收端。

    假设发送端需要发送两条数据“别紧张,你这样没事的!”和“好好看文章,你一定可以学会”。首先会把这两条数据放到发送缓冲区中,然后在经过网卡进行数据的发送到接收方的接收缓冲区中。如果接收方没有及时从接收缓冲区中获取往外取数据,那么数据就会在缓冲区挤压,这样两条数据就会积压在一块,就成了一条数据,这就是粘包的问题!

    那么什么是拆包问题呢?拆包问题是TCP每次发送的长度是有限制的,如果发送一个包的数据过大的话,TCP就会把这个包拆成两个包来进行发送。

    假设要发送的数据“别紧张,你这样没事的!”很大,TCP在发送的时候把它拆成了“别紧张,你这样”和“没事的!”进行发送,那么在接收方就会收到两个报文,这就是拆包的问题。

    实际上过大的话,还有可能会被拆成三个或者更多的包进行发送。但是无论被拆成几个包,TCP都能够保证发送包的顺序性和正确性。

    那么产生粘包和拆包的原因是什么呢?这个和TCP的缓冲区与滑块窗口、MSS/MTU限制、Nagle算法有关。

    有了粘包和拆包的问题,我们在实际的开发中应该怎么避免或者处理这个问题呢?那就是定义我们的通讯协议。这样如果粘包了就可以根据协议来区分不同的包,如果拆包了就等待数据构成一个完整的消息之后在进行处理。

    第一种方式---定长协议:所谓的定长协议就是指定一个报文的固定长度,每次双方按照约定截取固定的长度。假设我们需要发送“hello”和“very”两个单词,按照约定的5个字节进行一次截取。那么不足5个字节的单词可以添加0作为补充,则发生的规则如下。

    由于不足约定长度的需要进行补0,因此定长协议会造成带宽的浪费。

    第二种方式---特殊字符分隔符:使用特殊字符分隔符就是在报文的结尾进行追加特殊字符分隔符,用次分隔符来标注这是一个完整的报文,例如遇到了“\n”。

    这样虽然可以对报文进行划分,但是要求就是报文中不能包含特殊分隔符。

    第三种方式---固定头长度:发送数据之前,需要先获取需要发送内容的二进制字节大小,然后在需要发送的内容前面添加一个固定长度头整数,表示消息体二进制字节的长度。

    这种方式避免了特殊字符带来的问题,是生产中可以采取的一个方式,我在之前的文章中有介绍过这样的使用方法。

    其实对于java程序员来说,我们不必过分关心接收和发送缓冲区,需要了解其概念,因为底层已经为我们做了封装。明白“粘包”和“拆包”发生的过程和原因。

    通过观察用户空间和内核空间的数据交互,你也许会发现进行一次完整的交互需要进行四次的数据拷贝,这在性能上可能会有所影响。这也就有了面试官经常问的“零拷贝”的问题,尝试着自己对本文的理解学习一下“零拷贝”,这是为后面学习Netty打下坚实的基础。

如何解决TCP拆包粘包问题

TCP协议是可靠的,数据包一定会到达(99.9%的情况下),而且是按顺序到达。

TCP是“流”协议,所谓“流”协议,就是没有界限,没有分割的一串数据。TCP会根据缓冲区实际情况进行划分,一个完整的包可能会拆分成多个包进行发送,也有可能把多个小包封装成一个大的数据包发送,这就是TCP粘包/拆包。

技术图片


发生原因

  • 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
  • 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
  • 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

解决方案

无论拆包还是粘包本质问题都是无法区分包界限,解决包界限的问题主要有以下几种方式:

  1. 消息数据的定长,比如定长100字节,不足补空格,接收方收到后解析100字节数据即为完整数据。但这样的做的缺点是浪费了部分存储空间和带宽。
  2. 消息数据使用特定分割符区分界限,比如使用换号符号做分割。
  3. 把消息数据分成消息头和消息体,消息头带消息的长度,接收方收到后根据消息头中的长度解析数据。

在实际开发中很多网络框架对TCP拆包粘包问题的解决做了很多支持,比如netty中LineBasedFrameDecoder解析器就是利用换号符号做分割。


参考文档

【1】浅谈TCP拆包粘包问题
【2】TCP粘包/拆包
【3】TCP粘包,拆包及解决方法






以上是关于TCP-缓冲区和粘包、拆包有啥关系?的主要内容,如果未能解决你的问题,请参考以下文章

Netty之路(二)TCP拆包/粘包问题

python 网络编程 缓冲和粘包

4-10:TCP协议之面向字节流和粘包问题

TCP粘包和拆包问题

如何解决TCP拆包粘包问题

TCP 粘包