详解 TCP(上)

陈皓大神写的真的太好了,这篇文章的基本上所有内容将转自他的文章

让我们来看看这张图

首先来了解每个部分的意义

  • 知道为什么有 65536 个端口吗?因为 TCP 头里面对于端口的存储只有 16 位。也就是

  • Sequence Number 是包的序号,用来解决网络包乱序

  • Acknowledgement Number 就是 ACK——用于确认收到,用来解决不丢包的问题

  • Window 又叫 Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控

  • TCP Flag ,也就是包的类型,主要是用于操控 TCP 的状态机的

其他部分解释在这里:

TCP 的三次握手和四次挥手

为什么建链接要 3 次握手,断链接需要 4 次挥手?

  • 对于建链接的3次握手,主要是要初始化 Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的 Sequence Number(缩写为 ISN:Inital Sequence Number)——所以叫 SYN,全称 Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
  • 对于4次挥手,发送方和接收方都需要 Fin 和 Ack。所以是 4 次。上面那个图是一方主动发 FIN 包,还有一种情况,两端同时发 FIN。就像这样:

另有一些需要注意的地方:

  • SYN、FIN 位置。在 TCP Flag 里面
  • 关于建连接时SYN超时。试想一下,如果 server 端接到了 client 发的 SYN 后回了 SYN-ACK 后 client 掉线了,server 端没有收到 client 回来的 ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server 端如果在一定时间内没有收到的 TCP 会重发 SYN-ACK。在 Linux 下,默认重试次数为 5 次,重试的间隔时间从 1s 开始每次都翻售,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共 31s,第 5 次发出后还要等 32s 才知道第 5 次也超时了,所以,总共需要 ,TCP 才会把断开这个连接。
  • 关于 SYN Flood 攻击。一些恶意的人就为此制造了 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 处理不过来干脆就直接拒绝连接了。
  • 关于ISN的初始化。ISN 是不能 hard code 的。RFC793 中说,ISN 会和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始。这样,一个 ISN 的周期大约是 4.55 个小时。因为,我们假设我们的 TCP Segment 在网络上的存活时间不会超过 Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要 MSL 的值小于 4.55 小时,那么,我们就不会重用到 ISN。
  • 关于 MSL 和 TIME_WAIT。通过上面的 ISN 的描述,相信你也知道 MSL(Maximum Segment Lifetime) 是怎么来的了。在彻底结束 TCP 之前,还有一个状态叫做 TIME_WAIT。这是一个超时设置时长是 2*MSL(RFC793定义了 MSL 为 2 分钟,Linux 设置成了 30s)为什么要这有 TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT 确保有足够的时间让对端收到了 ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好 2 个 MSL,主动结束方就又会重新发一个 ACK。2)有足够的时间让这个连接不会跟后面的连接混在一起(这个就不太懂了)
  • 关于 TIME_WAIT 数量太多。在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫 tcp_tw_reuse,另一个叫 tcp_tw_recycle 的参数,这两个参数默认值都是被关闭的,后者 recyle 比前者 resue 更为激进,resue 要温柔一些。另外,如果使用 tcp_tw_reuse,必需设置 tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让TCP连接出一些诡异的问题
    1. 关于tcp_tw_reuse。官方文档上说 tcp_tw_reuse 加上 tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要tcp_timestamps在两边都被打开(你可以读一下tcp_twsk_unique的源码 )。我个人估计还是有一些场景会有问题。
    2. 关于 tcp_tw_recycle。如果是 tcp_tw_recycle 被打开了话,会假设对端开启了 tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个 NAT 网络的话(如:一个公司只用一个 IP 出公网)或是对端的 IP 被另一台重用了,这个事就复杂了。建链接的 SYN 可能就被直接丢掉了(你可能会看到 connection time out 的错误)(如果你想观摩一下Linux的内核代码,请参看源码 tcp_timewait_state_process)、
    3. 关于tcp_max_tw_buckets。这个是控制并发的 TIME_WAIT 的数量,默认值是 180000,如果超限,那么,系统会把多的给 destory 掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗 DDoS 攻击的。也说的默认值 180000 并不小。这个还是需要根据实际情况考虑。

Again,使用tcp_tw_reuse和tcp_tw_recycle来解决TIME_WAIT的问题是非常非常危险的,因为这两个参数违反了TCP协议(RFC 1122)

数据传输中的 Sequence Number

SeqNum 的增加是和传输的字节数相关的。上图中,三次握手后,来了两个 Len:1440 的包,而第二个包的 SeqNum 就成了 1441。然后第一个 ACK 回的是 1441,表示第一个 1440 收到了。

注意:如果你用 Wireshark 抓包程序看 3 次握手,你会发现 SeqNum 总是为 0,不是这样的,Wireshark 为了显示更友好,使用了 Relative SeqNum ——相对序号,你只要在右键菜单中的 protocol preference 中取消掉就可以看到“Absolute SeqNum”了

TCP 重传机制

TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。

比如:发送端发了 1,2,3,4,5 五个包,接收端收到了 1,2 于是返回 ack 3,然后收到了 4(3 没收到)。此时的 TCP 会怎么办?因为正如前面所说的,SeqNum 和 Ack 是以字节数为单位,所以 ack 的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

超时重传机制

有这样一个简单的办法:不回 ack,死等 3。当发送方发现收不到 3 的 ack 超时后,会重传 3。一旦接收方收到 3 后,会 ack 回 4——意味着 3 和 4 都收到了。

但是这样有个非常大的 BUG,不回 ACK 那收到的 4,5 也不告诉发送方,这样发送方很有可能会认为 4,5 也没有到。导致 4,5 的重传

快速重传机制

于是,TCP引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的ack,就重传。Fast Retransmit 的好处是不用等 timeout 了再重传。

比如说:

我收到了 3 没收到 2,返回 ack2

我又收到了 4 但还是没收到 2,返回 ack2

但是 TMD 我又收到了 5 就是没收到 2,还是返回 ack2

这个时候,不用等 timeout 的发送方就知道了 2 怕是掉了。于是会重新发 2。然后我接收到了我就返回 ack6

**快速重传只解决了一个问题:不再需要等 timeout 就可以重新传包了。那重传多少呢?我知道 4 丢了,那要不要重传 5,6,7 呢? **

SACK 方法

所以就有了另一个更好的办法:Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版。参看下图:

这样,在发送端就可以根据回传的 SACK 知道哪些数据到了,哪些数据没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过 tcp_sack 参数打开这个功能(Linux 2.4后默认打开)。

这里还需要注意一个问题——接收方 Reneging,所谓 Reneging 的意思就是接收方有权把已经报给发送端 SACK 里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖 SACK,还是要依赖 ACK,并维护 Time-Out,如果后续的 ACK 没有增长,那么还是要把 SACK 的东西重传,另外,接收端这边永远不能把 SACK 的包标记为 Ack。

注意:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》

Duplicate SACK - 重复收到数据的问题

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了

D-SACK 使用了 SACK 的第一个段来做标志

  • 如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK

  • 如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK

示例一:ACK 丢包

下面的示例中,丢了两个 ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为 ACK 都到了 4000 意味着收到了 4000 之前的所有数据,所以这个 SACK 就是 D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是 ACK 包。

示例二,网络延误

下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到 ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为 ACK 已到了3000,所以,这个 SACK 是D-SACK——标识收到了重复的包。

这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延时了。

可见,引入了D-SACK,有这么几个好处:

  • 可以让发送方知道,是发出去的包丢了,还是回来的 ACK 包丢了。
  • 是不是自己的 timeout 太小了,导致重传
  • 网络上出现了先发的包后到的情况(又称 reordering)
  • 网络上是不是把我的数据包给复制了

知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。Linux 下的 tcp_dsack 参数用于开启这个功能(Linux 2.4后默认打开)

陈皓大神讲的真的非常非常好,我仔仔细细把这篇文章过了一遍。

你可能感兴趣的:(详解 TCP(上))