GeekIBLi

来自TCP协议的灵魂拷问

2021-09-10

1. TCP 是用来解决什么问题?

2. 为什么要 TCP,IP 层实现控制不行么?

之所以要提取出一个 TCP 层来实现控制是因为 IP 层涉及到的设备更多,一条数据在网络上传输需要经过很多设备,而设备之间需要靠 IP 来寻址。

假设 IP 层实现了控制,那是不是涉及到的设备都需要关心很多事情?整体传输的效率是不是大打折扣了?

3. 连接到底是什么?

我们已经知道了为什么需要独立出 TCP 这一层,并且这一层主要是用来干嘛的,接下来就来看看它到底是怎么干的。

我们都知道 TCP 是面向连接的,那这个连接到底是个什么东西?真的是拉了一条线让端与端之间连起来了?

所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。

4. TCP报文头是什么样的

image-20210910200950214

首先可以看到 TCP 包只有端口,没有 IP。

 Seq 就是 Sequence Number 即序号,它是用来解决乱序问题的

ACK 就是 Acknowledgement Numer 即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦

标志位就是 TCP flags 用来标记这个包是什么类型的,用来控制 TPC 的状态。

窗口就是滑动窗口,Sliding Window,用来流控。

5. 三次握手流程是怎么样的

为什么要握手?

其实主要就是为了初始化Seq Numer,SYN 的全称是 Synchronize Sequence Numbers,这个序号是用来保证之后传输数据的顺序性。

image-20210910201233569

6. 第一次握手seq是怎么取值的

不知道大家有没有想过 ISN 的值要设成什么?代码写死从零开始?

想象一下如果写死一个值,比如 0 ,那么假设已经建立好连接了,client 也发了很多包比如已经第 20 个包了,然后网络断了之后 client 重新,端口号还是之前那个,然后序列号又从 0 开始,此时服务端返回第 20 个包的ack,客户端是不是傻了?

所以 RFC793 中认为 ISN 要和一个假的时钟绑定在一起ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕

所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。

7. SYN 超时了怎么处理?

也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,怎么办?

我脑海中一想到的就是重试,但是不能连续快速重试多次,你想一下,假设 client 掉线了,你总得给它点时间恢复吧,所以呢需要慢慢重试,阶梯性重试

在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接。

8. SYN泛洪攻击是什么?

你看到没 SYN 超时需要耗费服务端 63s 的时间断开连接,也就说 63s 内服务端需要保持这个资源,所以不法分子就可以构造出大量的 client 向 server 发 SYN 但就是不回 server。

使得 server 的 SYN 队列耗尽,无法处理正常的建连请求。

所以怎么办?如何解决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 队列满了直接拒绝连接。

9. 什么是四次挥手?

image-20210910202311830

那么所有的断开链接都是上面锁描述的那样吗?

可以看到👇双方都主动发起断开请求所以各自都是主动发起方,状态会从 FIN_WAIT_1 都进入到 CLOSING 这个过度状态然后再到 TIME_WAIT。

image-20210910202609442

10. 挥手一定需要四次吗?

假设 client 已经没有数据发送给 server 了,所以它发送 FIN 给 server 表明自己数据发完了,不再发了,如果这时候 server 还是有数据要发送给 client 那么它就是先回复 ack ,然后继续发送数据。

等 server 数据发送完了之后再向 client 发送 FIN 表明它也发完了,然后等 client 的 ACK 这种情况下就会有四次挥手。

那么假设 client 发送 FIN 给 server 的时候 server 也没数据给 client,那么 server 就可以将 ACK 和它的 FIN 一起发给client ,然后等待 client 的 ACK,这样不就三次挥手了?

11. 为什么要有 TIME_WAIT?

断开连接发起方在接受到接受方的 FIN 并回复 ACK 之后并没有直接进入 CLOSED 状态,而是进行了一波等待,等待时间为 2MSL。

MSL 是 Maximum Segment Lifetime,即报文最长生存时间,RFC 793 定义的 MSL 时间是 2 分钟,Linux 实际实现是 30s,那么 2MSL 是一分钟。

那么为什么要等 2MSL 呢?

  • 就是怕被动关闭方没有收到最后的 ACK,如果被动方由于网络原因没有到,那么它会再次发送 FIN, 此时如果主动关闭方已经 CLOSED 那就傻了,因此等一会儿。
  • 假设立马断开连接,但是又重用了这个连接,就是五元组完全一致,并且序号还在合适的范围内,虽然概率很低但理论上也有可能,那么新的连接会被已关闭连接链路上的一些残留数据干扰,因此给予一定的时间来处理一些残留数据。

12. 等待 2MSL 会产生什么问题?

如果服务器主动关闭大量的连接,那么会出现大量的资源占用,需要等到 2MSL 才会释放资源。

如果是客户端主动关闭大量的连接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 个,如果端口耗尽了就无法发起送的连接了,不过我觉得这个概率很低,这么多端口你这是要建立多少个连接?

对于服务端来说就是资源得不到立即释放,对于客户端来说,就是端口不能立即释放来发起新的链接。

13. 超时重传机制是为了解决什么问题?

前面我们提到 TCP 要提供可靠的传输,那么网络又是不稳定的如果传输的包对方没收到却又得保证可靠那么就必须重传。

TCP 的可靠性是靠确认号的,比如我发给你1、2、3、4这4个包,你告诉我你现在要 5 那说明前面四个包你都收到了,就是这么回事儿。

不过这里要注意,SeqNum 和 ACK 都是以字节数为单位的,也就是说假设你收到了1、2、4 但是 3 没有收到你不能 ACK 5,如果你回了 5 那么发送方就以为你5之前的都收到了。

所以只能回复确认最大连续收到包,也就是 3。

14. 为什么还需要快速重传机制?

超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。

于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。

因为连续收到三次相同 ACK 证明当前网络状况是 ok 的,那么确认是丢包了,于是立马重发,没必要等这么久。

就是发送方收到接收方连续的ACK报文之后,马上重传报文。

14. SACK 的引入是为了解决什么问题?

SACK 即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。

image-20210910204103120

SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。

如图,通过 ACK 告知我接下来要 5500 开始的数据,并一直更新 SACK,6000-6500 我收到了,6000-7000的数据我收到了,6000-7500的数据我收到了,发送方很明确的知道,5500-5999 的那一波数据应该是丢了,于是重传。

而且如果数据是多段不连续的, SACK 也可以发送,比如 SACK 0-500,1000-1500,2000-2500。就表明这几段已经收到了。

15. 滑动窗口是什么

我们已经知道了 TCP 有序号,并且还有重传,但是这还不够,因为我们不是愣头青,还需要根据情况来控制一下发送速率,因为网络是复杂多变的,有时候就会阻塞住,而有时候又很通畅。

所以发送方需要知道接收方的情况,好控制一下发送的速率,不至于蒙着头一个劲儿的发然后接受方都接受不过来。

因此 TCP 就有个叫滑动窗口的东西来做流量控制,也就是接收方告诉发送方我还能接受多少数据,然后发送方就可以根据这个信息来进行数据的发送。

以下是发送方维护的窗口,就是黑色圈起来的。

image-20210910204451140

图中的 #1 是已收到 ACK 的数据,#2 是已经发出去但是还没收到 ACK 的数据,#3 就是在窗口内可以发送但是还没发送的数据。#4 就是还不能发送的数据。

16. 如果接收方回复的窗口一直是 0 怎么办?

上文已经说了发送方式根据接收方回应的 window 来控制能发多少数据,如果接收方一直回应 0,那发送方就杵着?

你想一下,发送方发的数据都得到 ACK 了,但是呢回应的窗口都是 0 ,这发送方此时不敢发了啊,那也不能一直等着啊,这 Window 啥时候不变 0 啊?

于是 TCP 有一个 Zero Window Probe 技术,发送方得知窗口是 0 之后,会去探测探测这个接收方到底行不行,也就是发送 ZWP 包给接收方。

17. 已经有滑动窗口了为什么还要拥塞控制?

前面我们提到了重传,如果不管网络整体的情况,肯定就是对方没给 ACK ,那我就无脑重传。

如果此时网络状况很差,所有的连接都这样无脑重传,是不是网络情况就更差了,更加拥堵了?

18. 拥塞控制怎么搞?

主要有以下几个步骤来搞:

1、慢启动,探探路。

2、拥塞避免,感觉差不多了减速看看

3、拥塞发生快速重传/恢复

19. 什么是 TCP 半连接队列和全连接队列?

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accepet 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

image-20210911125838391

20. 如何查看全链接队列

ss -lnt | grep port

查看全链接队列溢出情况:

netstat -s |

参考资料

Tags: TCP