TCP的连接和关闭

由问题驱动,看一下TCP连接的本质吧。

tcp报文头.png

1. TCP如何保证消息的有序和不丢包?

Sequence Number就是SYN——包的序号,用来解决网络包乱序(reordering)问题。
Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。

2. 三次握手的目的?

为了建立可靠的数据传输,TCP通信双方相互告知初始化序列号(ISN),并确定对方已经收到ISN的(使用ACK机制)。

3. 三次握手的过程?

  1. 客户端发送一个SYN段,并指明客户端的初始序列号,即ISN(c).
  2. 服务端发送自己的SYN段作为应答,同样指明自己的ISN(s)。为了确认客户端的SYN,将ISN(c)+1作为ACK数值。这样,每发送一个SYN,序列号就会加1. 如果有丢失的情况,则会重传。
  3. 为了确认服务器端的SYN,客户端将ISN(s)+1作为返回的ACK数值。
三次握手的过程.png

4. 为什么不能用两次握手进行连接?

三次握手的目的就是为了建立可靠的连接,TCP通信双方都必须维护一个序列号,以标识发送的包哪些是被对方收到的。三次握手的过程中通信双方要相互告知初始化序列号,并确定对方已经收到。

如果只是两次握手,至多只有连接发起方的初始化序列号(ISN)能够被确认,另一方的序列化得不到确认。

三次握手.png

5. 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

四次挥手.png

四次挥手过程:

  1. 主机A发送FIN后,进入终止等待状态,服务器B收到主机A连接的释放报文,就立即给主机A发送ACK。然后服务器B就进入了close-wait状态。

  2. 并且服务器B再次发送FIN通知主机A关闭连接,服务器B进入最后确定状态。

  3. 主机A收到服务器BFIN请求后,会发送一个ACK告诉服务器B收到,于是客户端和服务器都关闭了。

FIN —— 该报文段的发送方已经结束向对方发送数据。

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方ACK和FIN一般都会分开发送。

一句话总结:是否关闭通道,是上层应用决定的的,TCP无权将FIN和ACK一同发送。

6. 三次握手建立连接时SYN超时?

server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发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才会把断开这个连接。

一句话总结:服务器未收到客户端的确定ACK,便会一直重试

7. 关于SYN攻击?

我们说过,在建立连接时,server未收到client端的ACK通知,便开始了长达63s的重试机制。

7.1 什么叫做SYN Flood:
一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。

7.2 妥协版的TCP协议:synccookies:
于是,Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。

7.3 对于正常的请求,你应该调整三个TCP参数可供你选择:
第一个是:tcp_synack_retries 可以用他来减少重试次数;
第二个是:tcp_max_syn_backlog,可以增大SYN连接数;
第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。

8. 如何设置ISN的值?

三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果ISN是固定的,攻击者很容易猜出后续的确认号。
ISN = M + F(localhost, localport, remotehost, remoteport)
M是一个计时器,每隔4微秒加1。 F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证hash算法不能被外部轻易推算得出。

一句话总结:ISN不能是固定不变的,一般是计时器(每4微妙+1)+随机hash值设置的,防止被攻击。

9、序列号回绕

因为ISN是随机的,所以序列号容易就会超过2^31-1. 而tcp对于丢包和乱序等问题的判断都是依赖于序列号大小比较的。此时就出现了所谓的tcp序列号回绕(sequence wraparound)问题。怎么解决?

内核代码:

/** The next routines deal with comparing 32 bit unsigned ints
 * and worry about wraparound (automatic with unsigned arithmetic).*/
static inline int before(__u32 seq1, __u32 seq2){
    return (__s32)(seq1-seq2) < 0;}

序列号发生回绕后,序列号变小,相减之后,把结果变成有符号数了,因此结果成了负数。

假设seq1=255, seq2=1(发生了回绕)。
seq1 = 1111 1111 seq2 = 0000 0001
我们希望比较结果是
 seq1 - seq2=
 1111 1111
-0000 0001
————————————
 1111 1110

由于我们将结果转化成了有符号数,由于最高位是1,因此结果是一个负数,负数的绝对值为
 0000 0001 + 1 = 0000 0010 = 2
因此seq1 - seq2 < 0

文章参考:

TCP 为什么三次握手而不是两次握手(正解版)