欢迎大家搜索“小猴子的技术笔记”关注我的公众号,领取丰富面试资料和学习资料。
你了解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打下坚实的基础。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。