TCP重点
三次握手,失败了会发生什么
第一次握手丢失
当客户端想和服务端建立 TCP 连接时,首先第一个发的就是 SYN 报文,然后进入 SYN-SENT
状态
如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制,重传 SYN 报文,而且 重传的 SYN 报文的序列号都是一样的
当服务端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,重发的次数由
tcp_syn_retries
内核参数控制,默认值一般是 5bash$ cat /proc/sys/net/ipv4/tcp_syn_retries 5
第一次超时重传是 1 秒后,第二次超时重传是 2 秒后,每次超时的时间是上一次的 2 倍
当第五次超时重是 16 秒,当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包了,然后断开 TCP 连接
总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右
具体过程:
- 当客户端超时重传 3 次 SYN 报文后,由于
tcp_syn_retries
为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接
第二次握手丢失
当服务端收到客户端第一次握手后,就会回 SYN-ACK 报文给客户端,此时服务端就会进入 SYN_RECD
状态
第二次握手的 SYN-ACK
报文其实有两个目的:
- 第二次握手里的 ACK,是对第一次握手的确认报文
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文
所以,如果第二次握手丢了,那么客户端就觉得自己的 SYN 报文(第一次握手)丢失了,于是 客户端就会触发超时重传机制,重传 SYN 报文
SYN-ACK 报文的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5
$ cat /proc/sys/net/ipv4/tcp_synack_retries
5
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由
tcp_syn_retries
内核参数决定 - 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由
tcp_synack_retries
内核参数决定
具体过程:
- 当客户端超时重传 1 次 SYN 报文后,由于
tcp_syn_retries
为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接 - 当服务端超时重传 2 次 SYN-ACK 报文后,由于
tcp_synack_retries
为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接
第三次握手丢失
客户端收到服务端的 SYN-ACK 报文后,就会给服务端返回一个 ACK 报文,也就是第三次握手,次数客户端进入 ESTABLISH
状态
因为第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以第三次握手丢失了,服务端就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数
注意:ACK 报文是不会重传的,当 ACK 丢失了,就由对方重传对应的报文
具体过程:
- 当服务端超时重传 2 次 SYN-ACK 报文后,由于
tcp_synack_retries
为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接
重传、滑动窗口、流量控制、拥塞控制
重传机制
TCP 实现可靠传输的方式之一,是通过序列号与确认应答
TCP 针对数据包丢失的情况,会用 重传机制 解决,常见如下重传机制
- 超时重传
- 快速重传
- SACK
- D-SACK
超时重传
- 当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据
TCP 会在以下两种情况发生超时重传
- 数据包丢失
- 确认应答丢失
超时时间应该设置为多少?
RTT
(Round-Trip Time 往返时延),指的是 数据发送时刻到接收到确认的时刻的差值- 超时重传时间是以
RTO
(Retransmission Timeout 超时重传时间)表示 - 超时重传时间 RTO 的值应该略大于报文往返 RTT 的值
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是 超时间隔加倍
也就是 每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?
于是就可以用「快速重传」机制来解决超时重发的时间等待
快速重传
TCP 还有另外一种 快速重传(Fast Retransmit)机制,它 不以时间为驱动,而是以数据驱动重传
- 第一份 Seq1 先送到了,于是就 Ack 回 2
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到
- 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2
- 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是 重传的时候,是重传一个,还是重传所有的问题
- 假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 Seq2、Seq3 都丢失了,那么接收方在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方
- 但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的, 那是选择重传 Seq2 一个报文,还是重传 Seq2 之后已发送的所有报文呢(Seq2、Seq3、 Seq4、Seq5、 Seq6) 呢?
- 如果只选择重传 Seq2 一个报文,那么重传的效率很低。因为对于丢失的 Seq3 报文,还得在后续收到三个重复的 ACK3 才能触发重传
- 如果选择重传 Seq2 之后已发送的所有报文,虽然能同时重传已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的报文是已经被接收过了,对于重传 Seq4 ~Seq6 折部分数据相当于做了一次无用功,浪费资源
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK
方法
SACK 方法
SACK
( Selective Acknowledgment), 选择性确认
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它 可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以 只重传丢失的数据
如果要支持 SACK
,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack
参数打开这个功能(Linux 2.4 后默认打开
Duplicate SACK
Duplicate SACK 又称 D-SACK
,其主要 使用了 SACK 来告诉「发送方」有哪些数据被重复接收了
D-SACK
有这么几个好处:
- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了
- 可以知道是不是「发送方」的数据包被网络延迟了
- 可以知道网络中是不是把「发送方」的数据包给复制了
在 Linux 下可以通过 net.ipv4.tcp_dsack
参数开启/关闭这个功能(Linux 2.4 后默认打开)
滑动窗口
TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个
- 这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的
- 所以,这样的传输方式有一个缺点:数据包的 往返时间越长,通信的效率就越低
为解决这个问题,TCP 引入了 窗口 这个概念
- 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据
- 如果按期收到确认应答,此时数据就可以从缓存区清除
TCP 头里有一个字段叫 Window
,也就是窗口大小
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来
流量控制
发送方不能无脑的发数据给接收方,要考虑接收方处理能力
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制
拥塞控制
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....
于是,就有了 拥塞控制,控制的目的就是 避免「发送方」的数据填满整个网络
拥塞窗口 cwnd 是发送方维护的一个的状态变量,它会根据 网络的拥塞程度动态变化的
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大 - 但网络中出现了拥塞,
cwnd
就减少
拥塞控制主要是四个算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复