# ❓谈谈你对 TCP 三次握手和四次挥手的理解

# 三次握手

其实就是指建立 TCP 链接时,需要客户端和服务器共发送三个包。进行三次握手的主要作用就是为了确认双方的接受能力和发送能力是否正常,指定自己的初始化序列号为 后面的可靠性传输作准备。实质上就是链接服务器指定端口,建立 TCP 链接,并同步链接双方的序列号和确认号,交换 TCP 窗口大小信息。

刚开始的时候客户端处于 Closed 状态,服务端处于 Listen 状态。

开始进行三次握手:

第一次握手:客户端给服务端发送一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。

首部的同步位 SYN = 1,初始化序列号 seq = x,SYN = 1 的报文不能携带收据,但要消耗掉一个序号。

第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且指定自己初始化序列号 ISN(s)。 同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

在确认报文中,SYN = 1,ACK = 1,确认号 ack = x + 1,初始化序列号 seq = y。

第三次握手:客户端接收到 SYN 报文之后,会向客户端发送一个 ACK 报文,也是把服务器的 ISN + 1 作为 ACK 的值,表示已经收到服务器 SYN 报文, 此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

确认报文段 ACK=1,确认号 ack = y + 1,序号 seq = x + 1(初始为 seq = x,第二个报文段所以要 + 1),ACK 报文段可以携带数据, 不携带数据则不消耗序号。

图解:

img

扩展

# 那么为什么要三次握手,两次不行吗?

那我们要弄清楚三次握手到底做了什么事情,能通过两次握手达到同样的目的吗?

  • 第一次握手:由客户端发送的包,服务端收到了

    那么证明客户端发送能力,和服务器的接受能力都是正常的。

  • 第二次握手:由服务端发送包,客户端接收到了。

    那么证明服务器发送能力,以及客户端的接受能力都是正常的。

    到这里证明的客户端的发送、接受能力,一节服务端的发送、接受能力都是正常的。但是不要忽略一点,就是到这里其实服务端还不知道,你 客户端的是否接收到服务端发送的内容,所以就有了第三次的握手。

  • 第三次握手:客户端发送包,服务端接收。

    那么服务端就可以确认自己以及客户端的发送、接受能力都是正常的。

# 什么半连接队列?

服务端第一次收到客户端的 SYN 请求之后,就会处于 SYN_RCVD 状态,此时的双方还没有完全建立连接,服务端会把这种状态下请求放到一个队列里面,我们称之为半连接队列。

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于 SYN-ACK 重传次数的问题:

服务器发送完 SYN-ACK 包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。

注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…

# ISN(Initial Sequence Number)是固定的吗?
  • 当一端为建立连接而发送它的 SYN 时,它为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。ISN 可以看作是一个 32 比特的计数器,每 4ms 加 1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。

  • 三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

# 三次握手过程中可以携带数据吗?

第三次握手的时候是可以携带收据的,但是第一第二次是不可以的。

  • 假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。

  • 也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

# SYN 攻击是什么?

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击。SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。

netstat -n -p TCP | grep SYN_RECV
1

常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies 技术

# 四次挥手

建立一个完整连接需要三次握手,但是关闭一个连接需要四次挥手(或者叫四次握手)。这其实是由于 TCP 的半关闭(half-close)导致的。所谓的半关闭就是 TCP 提供连接的一端在结束它的发送后还能接收来自另一端数据的能力 --> TCP 连接是全双工的(通道 1:客户端的输出连接服务端的输入;通道 2:客户端的输入连接服务端的输出),在两个方向上都能传输数据,因此两个方向就需要单独关闭。

TCP 的连接的关闭需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。

刚开始的时候双方都处于 ESTABLISHED 状态,假如是客户端先发起挥手动作,那么四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。

    即发出连接释放报文段(FIN = 1,序号 seq = u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN_WAIT1(终止等待 1)状态,等待服务端的确认。

  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。

即服务端收到连接释放报文段后即发出确认报文段(ACK = 1,确认号 ack = u + 1,序号 seq = v),服务端进入 CLOSE_WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入 FIN_WAIT2(终止等待 2)状态,等待服务端发出的连接释放报文段。

  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN = 1,ACK = 1,序号 seq = w,确认号 ack = u + 1),服务端进入 LAST_ACK(最后确认)状态,等待客户端的确认。

  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK = 1,seq= u + 1,ack= w + 1),客户端进入 TIME_WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,客户端才进入 CLOSED 状态。

收到一个 FIN 只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入 TIME_WAIT 是正常的,服务端通常执行被动关闭,不会进入 TIME_WAIT 状态。

图解:

img

扩展

# 挥手为什么需要四次?

因为当服务端收到客户端的 SYN 连接请求报文后,可以直接发送 SYN + ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当服务端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉客户端,“你发的 FIN 报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四次挥手。

# 2MSL 等待状态?

TIME_WAIT 状态也成为 2MSL 等待状态。每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为 TCP 报文段以 IP 数据报在网络内传输,而 IP 数据报则有限制其生存时间的 TTL 字段。

对一个具体实现所给定的 MSL 值,处理的原则是:当 TCP 执行一个主动关闭,并发回最后一个 ACK,该连接必须在 TIME_WAIT 状态停留的时间为 2 倍的 MSL。这样可让 TCP 再次发送最后的 ACK 以防这个 ACK 丢失(另一端超时并重发最后的 FIN)。

这种 2MSL 等待的另一个结果是这个 TCP 连接在 2MSL 等待期间,定义这个连接的插口(客户的 IP 地址和端口号,服务器的 IP 地址和端口号)不能再被使用。这个连接只能在 2MSL 结束后才能再被使用。

# 四次挥手释放连接时,等待 2MSL 的意义?

MSL 是 Maximum Segment Lifetime 的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为了保证客户端发送的最后一个 ACK 报文段能够到达服务器。因为这个 ACK 有可能丢失,从而导致处在 LAST-ACK 状态的服务器收不到对 FIN-ACK 的确认报文。服务器会超时重传这个 FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待 2MSL,而是在发送完 ACK 之后直接释放关闭,一但这个 ACK 丢失的话,服务器就无法正常的进入关闭连接状态。

  • 保证客户端发送的最后一个 ACK 报文段能够到达服务端。

  • 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

# 为什么 TIME_WAIT 状态需要经过 2MSL 才能返回到 CLOSE 状态?

理论上,四个报文都发送完毕,就可以直接进入 CLOSE 状态了,但是可能网络是不可靠的,有可能最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。

参考

面试官,不要再问我三次握手和四次挥手

谈谈你对 TCP 三次握手,四次挥手的理解