最近在处理一个TCP粘包问题,虽然相关的知识都略知一二,但是好像让我完整的说出来细节,却还要查下资料,因此,整理列出来,以便复习。而且这些知识点是后台开发人员的必备基本功,相当于内功,还是要多拿出来温故而知新。
1.不同协议层的限制值:
1.1 链路层(以太网)
局域网(无线是另一种协议)采用CSMA/CD协议,核心概念就是
通道共用,发送前监听冲突,冲突后重发
标准以太网帧长度下限:64byte
原理大概就是因为发送前要监听是否有其他人在使用通道,而根据通讯原理,64byte传输的时间就是知道是否有冲突的最小时间。
标准以太网帧长度上限:1518byte
这个值也是协议标准协会“拍脑袋”的结果。
一方面,如果太小,每次传输的利用率就低(TCP头部20byte,IP头部20byte,链路层头部18byte);另一方面,如果太大,就会导致共享通道被某一方霸占太久,而且,包太大,出错率高,缓存成本也要提高。
1.2 网络层
MTU: 1500byte(TCP标准化采用576byte)
因为“标准以太网帧长度上限”为1518byte,所以减去链路层18byte的头部,IP数据报的最大限制值就等于1500byte。
IP分片
注意和传输层的“数据报分段”区分。
如果IP数据报长度大于MTU,就需要分成几段传输,然后重组,分片和重组都是在网络层协议完成的。
因为TCP传输层才有提供超时和重传机制,网络层没有,所以如果有一片IP分片丢失,需要重传(TCP的重传),丢失的IP分片所属的TCP数据的所有IP分片,都要再重新传输。
1.3 传输层(TCP)
MSS
每个TCP数据报(去掉IP头部和TCP头部)的最长长度。如果未设置,默认为536byte(对应MTU576byte),一般在以太网,都是1460byte(1500-20-20)。
在TCP握手的前两次商议MSS值。
TCP分段
请区分 “TCP分段” 和 “IP分片”,两个是在不同协议层的概念。
如果TCP数据长度超过MSS,就要分段,然后重组。
因为MSS<MTU,所以TCP一般没有IP分片,而UDP和ICMP没有分段,所以就只能靠IP分片了。
2. Nagle 算法
Nagle算法
为了解决telnet场景下,敲一下键盘就发送一个字符,传输效率低导致网络阻塞的问题。
Nagle算法基本定义是:任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
可以设置TCP_NODELAY来关闭,提高传输效率,缺点是实时交互性差。
TCP确认延迟机制
可以看出,Nagle算法受限于接收方发回ACK的时间间隔,因此与之配套的接收方使用“TCP确认延迟机制”。延迟默认为40ms,视情况调整数值。可以设置TCP_QUICKACK标志来关闭。
3. TCP粘包
为什么会出现TCP粘包?
在Nagle算法的作用下,假设在等待ACK期间,发送一段文字“hello”,然后又发送了“world”,假设MSS为7,这时对方接收到的就是“hellowo”。消息黏在一起,所以称为“粘包”。
解决方法:一般是使用标识来识别界限。加字段size来指明一个包的长度,或者是在一条数据的结尾加特殊的结束符(比如/n)。
为什么UDP不会粘包?
首先,在发送方,UDP没有Nagle算法来合并发送几个数据。
然后接收方,UDP是基于消息保护边界,不同于TCP的基于流传输。也就是UDP会使用链式保持每条接收的完整消息。比如发送方发了3条消息,UDP接收方会在缓存独立保存3条消息,而TCP则可能只接收到一条合并的消息;UDP需要recv 3次才能接收3条消息,TCP则可能recv一次就行了。
为什么UDP会采用链式保存接收到的消息?因为TCP是一对一的连接,而UDP是可能一对多的连接,为了区分不同发送方发来的消息,所以需要分开存放。
4. 为什么UDP没有发送缓冲区
首先需要大概了解一下TCP和UDP在执行write的原理
TCP在write的时候,系统把消息从用户态拷贝到内核的socket发送缓冲区,然后直接返回成功,接下来会有一系列操作会用到缓冲区(丢失重传,Nagle算法的合并,滑动窗口等等)。直到收到对方的ACK,内核才会把消息从发送缓冲区删除。
UDP在write的时候,因为是不可靠传输,所以只是把数据从用户态拷贝到内核,然后立即发送出去,不管对方是否能收到,所以不需要缓冲区。
TCP和UDP都有接收缓冲区
内核态和用户态的切换工作模式,决定了缓冲区的必要性。当然两种缓冲区的工作模式不一样。
TCP如果缓冲区满了,会通过滑动窗口的流量控制告诉对方不要再发了,不然会丢掉新包。UDP则只是简单的丢掉。
从缓冲区理解socket的阻塞和非阻塞操作
从上面可以看到,不管是read或者write,操作其实都是消息在用户态和内核态的拷贝。因此,阻塞其实就是看内核缓冲区有没有数据。
比如阻塞模式下,read会一直等待内核缓冲区有数据才返回,而非阻塞模式,则会立即返回;阻塞模式下,write会一直等待内核有足够空间写入数据才返回,而非阻塞模式,有空间则写入,没有就会告诉你失败。
5. 三次握手和四次挥手
为什么需要3次握手
因为是建立一个全双工的连接,所以互相都要得到对方的确认,以及通信的双方要互相通知对方自己的初始化的Sequence Number,所以流程顺序应该是这样的:(1.A跟B说我要连接;2.B答应并且返回x+1;3.B跟A说要建立连接;4.A答应并且返回y+1。)
总共有4个步骤,但是因为步骤2和3可以合并一起,所以就成了“3次握手”。
注意:SeqNum的增加是和传输的字节数相关的,握手和挥手都是1个字节,所以才加一。
如果A在执行步骤4之前就断开了,B会重发步骤3。在Linux下,默认重试次数为5次,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
SYN flood攻击
给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。
于是,Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。
对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。
5. TCP滑动窗口
TCP滑动窗口是网络流控的一种实现方法。接收方通过通知发送方自己的窗口大小,从而控制发送方的发送速度,达到防止发送方发送速度过快而导致自己被淹没的目的。
由上图可以看出窗口的动态变化过程:
1.一开始窗口大小360,Client发送140长度的数据给Server,Server接收返回ACK并调整窗口为260;
2.Client接收到ACK并调整窗口为260,然后发送180的数据给Server,Server接收后返回ACK又调整窗口为80;
3.Client接收到ACK并调整窗口为80,然后发送80的数据给Server,Server接收后返回ACK调整窗口为0;
TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。
其中各自的“接收窗口”大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。
各自的“发送窗口”则要求取决于对端通告的“接收窗口”,要求相同。
好了,差不多就这些了,好像还有非常多没有涉及,不过相信把这些都理解好了,已经足够应付大多数的面试了。
以后有时间再整理一个应用层协议方面的文章。
网友评论