TCP常见问题总结
TCP常见问题
TCP全称Transmission Control Protocol,即传输控制协议。TCP控制的内容主要包括:
- 可靠性
- 有序性
- 流量控制
- 拥塞控制
为何不在IP层对数据进行上述控制?
不在IP层实现控制是因为IP层涉及到的设备很多,设备之间靠IP来寻址,如果在IP层实现控制,那么涉及到的设备都要关心很多事情,整体的传输效率会收到影响。
TCP所谓的连接只是双方都维护了一个状态
TCP协议头
如图:
- TCP包只有端口,没有IP
- Seq就是Sequence Number即序号,用来解决乱序问题
- ACK就是Acknowledgement Number,即确认号,用来解决丢包的情况,告知发送方接收到的包的序号
- 标志位就是TCP flags,用来标记包的类型,用来控制TCP的状态
- 窗口就是滑动窗口Sliding Window,用来进行流量控制
三次握手
目的有二:
- 确认双方的发送接收功能都正常
- 初始化Seq Number,SYN的全称为Synchronize Sequence Numbers,这个序号是用来保证传输数据的正确性
初始序号ISN的取值
如果ISN从0开始,假设建立好连接之后发送了第20个包之后网络断了,client重启了,序号又从0开始,此时服务端返回第20个包的ack客户端就没法处理了。
所以RFC739中认为ISN要和一个假设的时钟绑定在一起:
ISN每四微秒加一,当超过2的32次方之后又从0开始,要四个半小时左右发生ISN回绕。
所以ISN变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法分子猜到ISN。
SYN超时如何处理
慢慢重试,阶梯性重试。
Linux中就是默认重试5次,并且间隔是1s、2s、4s、8s、16s,在第五次发出后还得再等32s才能知道这次重试的结果,总共等63秒才能断开连接。
SYN Flood攻击
client向server发送SYN但就是不回server,最后使得server的SYN(半连接)队列耗尽,无法处理正常的建立连接的请求。
如何应对?
可以开启tcp_syncookies,这样就用不到SYN队列了。SYN队列满了之后TCP根据自己的ip、端口然后向对方的ip、端口,对方SYN的序号,时间戳等一波操作生成一个特殊的序号(即cookie)发回去,如果对方是正常的client会把这个序号发回来,然后server根据这个序号建立连接。
或者调整tcp_synack_retries减少重试的次数,设置tcp_max_syn_backlog增加SYN队列数,设置tcp_abort_on_overflow, SYN队列满了直接拒绝连接。
为什么要四次挥手?
因为TCP是全双工协议,也就是说双方都要关闭,每一方都想对方发送FIN和回应ACK,所以看起来就是四次。
主动关闭方的状态是FIN_WAIT_1到FIN_WAIT_2,然后再到TIME_WAIT状态。
被动关闭方是CLOSE_WAIT到LAST_ACK状态。
为什么要有TIME_WAIT?
主动断开方在接收到被动关闭方的FIN并回复ACK之后并没有直接进入CLOSED状态,而是等待了2MSL。
MSL是Maximum Segment Lifetime,即报文最长生存时间,RFC793定义的MSL时间是2分钟,Linux实际实现是30s,那么2MSL是1分钟。
等待2MSL会产生什么问题?
如果服务器主动关闭大量连接,那么会出现大量的资源占用,需要等到2MSL才会释放资源。
如何解决2MSL产生的问题?
快速回收,即不等2MSL就回收,Linux的参数是tcp_tw_recycle,还有tcp_timestamps不过是默认打开的。
重用,即开启tcp_tw_reuse当然也是需要tcp_timestamps的。tcp_tw_reuse是用在连接发起方的,而我们的服务端基本上是被动接收连接方。
tcp_tw_reuse是发起新连接的时候可以复用超过1s的处于TIME_WAIT状态的连接,所以对服务端压力减小的效果甚微。它重用的是发起方处于TIME_WAIT的连接。
这里还有一个SO_REUSEADDR,这是一个用户态选项,而tcp_tw_reuse是一个内核选项。然后SO_REUSEADDR主要用在启动服务的时候,如果此时端口被占用了并且这个连接处于TIME_WAIT状态,那么可以重用这个端口,如果不是TIME_WAIT状态,抛出Address already in use。
所以对于服务端,tcp_tw_recycle和tcp_tw_reuse都不太好用。
所以服务端尽量不要主动关闭连接,把关闭连接的请求放到服务端来做,减少服务端出现TIME_WAIT状态的连接出现的机率,提高服务端的资源利用率。
超时重传机制是为了解决什么问题?
解决了丢包问题。
为什么还需要快速重传机制?
超时重传是靠时间来驱动的,如果网络状况不好,超时重传没问题,如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。所以引入了快速重传,如果发送方连续三次收到对方相同的确认号,就可以证明网络状况没问题,确认是丢包了,那么马上重传数据。
SACK的引入是为了解决什么问题?
如果发送方发送了1、2、3、4这四个包,就2对方没收到,然后不管是超时重传还是快速重传反正对方就回ACK 2,这时候是要重传2、3、4呢,还是只传2?
SACK的引入就是为了解决发送方不直到该重传哪些数据的问题。
如图所示,SACK就是接收方会回传它已经接收到的数据,这样发送方就知道哪一些数据对方已经收到了。
D-SACK又是啥?
D-SACK是SACK的扩展,它利用SACK的第一段来描述重复接收的不连续的数据序号,如果第一段描述的范围被ACK覆盖了,说明重复了,比如我都ACK到6000了你还给我回SACK 5000-5500呢?
参数是tcp_dsack,Linux2.4之后默认开启。
那直到重复了有什么用呢?
- 知道重复了说明对方收到刚才那个包了,所以是回来了ACK包丢了。
- 是不是包乱序的,先发的包后到?
- 是不是自己太着急了,RTO太小了?
- 是不是被数据复制了,抢先一步呢?
滑动窗口的作用?
滑动窗口用来做流量控制,接收方告诉发送方他还能接收多少数据,然后发送方就可以根据这个信息来控制数据的发送。
如果接收方回复的窗口一直是0怎么办?
TCP有一个Zero Window Probe技术,发送方得知窗口是0之后,回去探测这个接收方到底行不行,也就是发送ZWP包给接收方,如果多次之后都不行可以直接RST,发送次数根据具体实现而定。
已经有滑动窗口了为什么还要有拥塞控制?
加拥塞控制是因为TCP不仅仅就管两端之间的情况,还需要知晓整体的网络情形。因为如果网络状况很差,所有的连接都无脑重传的话,网络将变的更差,更加拥堵。所以需要拥塞控制来避免这种情况的发生。
如何实现拥塞控制?
主要有以下几个步骤:
- 慢启动
- 拥塞避免,感觉差不多了减速看看
- 拥塞发生后,快送重传/回复
慢启动,一开始初始化cwnd(congestion window)为1,然后每收到一个ACK就cwnd++,并且没过一个RTT就cwnd = 2*cwnd。
然后到了一个阈值,也就是ssthresh(slow start threshold)的时候就进入拥塞避免阶段。这个阶段就是每收到一个ACK就cwnd = cwnd+1/cwnd并且每一个RTT就cwnd++。
然后一直线性增加,直到开始丢包的时候,一种是超时重传,一种是快速重传。
如果发生超时重传,直接将ssthresh置为当前cwnd的一半,然后cwnd直接变为1,进入慢启动阶段。
如果是快速重传,那么有两种实现,一种是TCP Tahoe,和超时重传一样的处理。一种是TCP Reno,这个实现是把cwnd=cwnd/2,然后把ssthresh设置为当前的cwnd。
然后进入快速恢复阶段,将cwnd = cwnd + 3(因为快速重传有三次),重传DACK指定的包,如果再收到一个DACK则cwnd++,如果收到的是正常的ACK那么就将cwnd设为ssthresh大小,进入拥塞避免阶段。
总结
TCP是面向连接的,提供可靠、有序的传输并且还提供流控和拥塞控制,单独提取出TCP层而不是在IP层实现是因为IP层有更多的设备需要使用,加了复杂的逻辑不划算。
三次握手主要是为了定义初始***为了之后的传输打下基础,四次挥手是因为TCP是全双工协议,需要双方都断开连接。
SYN超时了就阶梯性重试,如果有SYN攻击,可以加大半连接队列,或减少重试次数,或直接拒绝。
还提供流控和拥塞控制,单独提取出TCP层而不是在IP层实现是因为IP层有更多的设备需要使用,加了复杂的逻辑不划算。
三次握手主要是为了定义初始***为了之后的传输打下基础,四次挥手是因为TCP是全双工协议,需要双方都断开连接。
SYN超时了就阶梯性重试,如果有SYN攻击,可以加大半连接队列,或减少重试次数,或直接拒绝。
TIME_WAIT是怕对方没收到最后一个ACK,然后又发了FIN过来,并且也是等待处理网络上残留的数据,怕影响新连接。