TCP 的那些事

222 阅读7分钟

首先要知道 TCP 在 OSI 的七层模型中的第四层 - 传输层(来自《图解 TCP/IP》)

再简单的看一下 客户端 和 服务端之间的数据传输(图来自极客时间

数据发送时经过每一层都会加上对应的协议头,接收端则会一层层解析头,交给高层的协议处理。

TCP 头格式

其中我觉得比较重要的几个内容是

  • 序号。为包编上号,解决包的乱序问题。
  • 确认序号,Acknowledgement Number。也就是我们常说的确认应答号 ACK ,可以解决丢包的问题。
  • Window。也就是我们常说的滑动窗口。
  • TCP Flags,状态位。

TCP 的状态机

TCP 就是靠改变,维持通讯双方的状态来保证他们之间的“连接”的。

例如,客户端发送 SYN ,就是期待发起连接,客户端就切换为 SYN-SENT 状态。服务端被动监听端口,处于 LISTEN 状态。Server 接收到 SYN 包后,也向 client 发送 SYN,以及对接收到的 SYN 的 ACK,此时 server 处于 SYN-RECEIVED 状态。 client 收到 server 的 SYN 以及对于自己之前 SYN 的 ACK 后,也要针对 server 的 SYN 发送 ACK,就变成 ESTABLISHED 状态。若此刻 server 成功收到最后这个 ACK ,也进入 ESTABLISHED 状态。

刚才描述的过程就是我们常说的建立连接时的三次握手。

对于三次握手,重要的点在于:

  • SYN seq = x seq = y,主要就是双方去确定 Sequence Numbers 的值。 这个号就是以后通信要用到的包的序号。(Synchronize Sequence Numbers,SYN

对于四次挥手断开连接,因为TCP连接是全双工(两方可以互相同时传输数据)的,所以当任何一方想要断开连接时,都不能那么任性。你可以保证自己没有数据要发送了,但是你不知道对方还有没有数据要继续发送。所以我理解为什么是四次,因为双方都需要像对方提出断开连接并收一下 ACK。

在建立连接和断开连接时会有各种复杂情况,以下说明一些常见的

  • 建立连接时,客户端发送 SYN 后,直接掉线。server 就收不到 client 对于自己 SYN 的 ACK。当然 server 会一直尝试发送 SYN-ACK。 在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
  • 断开连接时的 time-wait 到 close 状态中间等待的一段 2MSL 时间,(Maximum Segment Lifetime)。因为双方在最后收到对方的 FIN 报文时,要给对方一个 ACK,让对方知道自己知道你要也要断开连接了。而不是发了 ACK 直接跑路,这样 B 就一直收不到自己 FIN 的 ACK。
  • 这个 MSL ,报文最大生存时间。可以理解成报文在网络中可以存活的最长时间,超过这个时间还没到达目的地,就会被丢弃。所有 A B 等待的 2MSL 时间还有一个原因就是,避免下一个占用了此端口的应用收到上次与自己无关连接的包。等那么久还没有收到包也就被丢弃了。

TCP 重传机制

TCP 保证可靠,稳定的传输,保证包全部顺利到达对方。但是网络世界很复杂,各种意外情况如何去保证呢?

其中一种就是前面提到的 ACK 机制。比如接收端收到 4000 的包,ACK 回去要 4001 之后的包,发送端就知道 4000 包成功到达了。

其中的意外情况有,1)接收端没收到 4000 的包,就一直 ACK 3999,发送端就知道要重发 4000 的包了。2)发送端没收到 ACK,就以为接收端没收到(实际上收到了),也重发 4000 的包。

累计应答

相比于一个一个包的发送,确认。实际上,接收端只会给发送端 ACK 收到的连续包的最后一个序号。比如发送端发送了 1-5 个包,接收端 ACK 一个 3 (x + 1,3这个包还没收到)给发送端。说明收到了 1,2 两个包。也就是累计应答

需要注意的是,seq 和 ACK 是以字节数来计算的,故不能跳着 ack。只能确认最大的连续收到的包。

引入窗口

简单的重传机制就是发一个等一个,效率低。

引入窗口的目的就是减少等待,在没有收到部分包 ACK 的情况下,允许发送最大的段。比如窗口被定义为 5,就允许最多连续发送 5 个段,而不是一个个等待。

重发控制

还有一种需要重发的情况是,发送端一直接收不到 ACK。TCP 就会等待一段时间,如果超过就重发。这个等待时间不宜超过 RTT(数据包往返的时间),否则可能进行不必要的重传。

重发的时候就涉及到一个问题,当发送端一直接收不到 3001 的 ACK 时(接收端确实没有收到 3000 的数据),而接收端收到了到 5001 的数据(放在缓冲区)。那么发送端到底是选择重发 3000 的数据,还是把 3001 ,4001, 5001 全部重发呢?

SACK

Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:

接收端不仅发送 ACK,还发送一个 SACK 向发送端说明自己缓冲区已经收到了 5000 的数据(只是还无法想你发送 5001 的 ACK ,因为在前面断了一截儿)。

但是,发送端不能把 SACK 作为真正意义上的 ACK 看待,因为接收端对于 SACK 的数据是可能放弃掉的。后续发送端如果检测到 ACK 没有实际性的增长,仍然需要重发该部分的数据。

滑动窗口,窗口控制

TCP 头中有一个 window 字段,又叫 Advertised-Window,这个字段的作用是接收端会告诉发送端自己能接受处理的最大数据,发送端会根据这个值调整发送的数据多少,避免接收端压力太大。

且这个 window 是有可能到 0 的。也就是说,发送端不再发送数据。

当 window 变成 0 的时候,发送方会定时发送窗口探测数据包,看看有没有增加 window 值的可能。一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

TCP 拥塞控制

前面讨论的窗口,关注的点是接收端的处理能力。这里的拥塞控制也有一个窗口的概念,但是关注的点是对于整个网络的影响。

我们知道,TCP 有超时重发机制,如果每个 TCP 不顾及整个网络的情况,不断的重发数据,网络状况因此可能更差,形成恶性循环。

慢启动

Congestion Window,cwnd。MSS(maximum segment size最大分段长度)

算法如下

  1. 连接建立,初始化 cwnd 为 1,说明可以传输一个 MSS 大小
  2. 每收到一个 ACK,cwnd++,线性增长
  3. 每过一个 RTT,cwnd = cwnd * 2,指数增长
  4. ssthresh(slow start threshold),当 cwnd >= ssthresh 时,进入“拥塞避免算法”

从算法过程可以判断,当网络状况良好的时候,ACK 的快,RTT 也快,这个慢启动也不算非常慢。网络状况差的话,那就是缓慢增长,到定义的阈值。

拥塞避免算法 – Congestion Avoidance

前面说过,还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:

1)收到一个ACK时,cwnd = cwnd + 1/cwnd

2)当每过一个RTT时,cwnd = cwnd + 1

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。

本文参考 :

酷壳 TCP 的那些事儿

极客时间 趣谈网络协议