TCP 的三次握手和四次挥手

TCP 协议做的是:在算法层面保证可靠性。

有关可靠性的指标:

  • 保证顺序
  • 不丢包
  • 维护 TCP 连接
  • 流量控制
  • 拥塞控制

为了能够保证以上指标,设计了对于的 TCP 包头格式,如下:

TCP 包头格式

TCP 三次握手基本流程

为了更好的理解 TCP 三次握手,来看一下生活中场景(A、B 两人彼此自我介绍的场景):


1、A (A -> B): 你好,我是 A(此时 A 不知道 B 是否收到自己的介绍,所以 A 现在等待 B 的 应答)。

2、B (B -> A): 你好,我是 B(此时 B 不知道 A 是否收到自己的介绍,所以 B 现在等待 A 对自己的应答进行应答,可称为 应答之应答)。

3、A (A -> B): 你好 B (此时 A 已经知道了 B,同时 B 也知道了 A , A 认为双方建立了连接,即使此次数据发送失败,当 A 开始发送数据时,也可完成连接)。


以上情况是在比较顺畅的情况两人建立了友谊关系,当然也会存在异常情况,如下几种:

异常情况一

步骤一中 A 发送的数据包丢失,因为各种原因:丢失、绕路、B 没有响应等情况,无法进入到步骤二,由于 A 没有收到发送数据包的应答,所以 A 会再次发送数据包,如果还是无法收到应答,还会再发。

异常情况二

A 发送的请求到达 B ,此时 B 已经知道了 A 的存在,如果 B 想要建立连接,就会发送应答给 A;如果 B 不想建立连接,就不会发送应答,A 在尝试发送数据包一段时间后就会放弃,此时建立连接失败。

异常请求三

在 A 多次发送请求数据包时,各步骤终于顺畅的进行下去,A、B 建立连接。 在两者进行短暂的交流后结束了谈话后,此时结束了连接,但是此时 A 建立连接时发送的数据包经过一段时间的绕路后,来到了 B ,如果 B 认为这是一个正常的请求的话建立了连接,此时这个连接不会进行下去,但是也不会有结束的时候,极大的浪费了网络资源。

这个异常情况在说明了 两次握手是不可以的

异常情况四

B 发送的应答可能会发送多次,但是只要有一次到达了 A ,那么 A 就会认为已经建立了连接,因为 对于 A 来说消息有去有回。当 A 为此次应答发送的应答到达 B 后,B 就认为已经建立了连接,因为 对于 A 来说消息也是有去有回

但是此时也会出现异常情况,如果 A 发送的应答消息丢失。按理来说,针对每一个应答都会有对方的应答之应答,依次循环往复,那么多少次握手都是可以的,但是并不能保证连接都是可靠的,所以 只要保证 A、B 双方的消息都有去有回就可以了,这也是为什么 TCP 建立连接需要三次握手的原因 了。

如果 A 发送的应答丢失了,此时对于 B 是怎样的情况? 好在 A 在建立连接后就会发送数据,如果 A 发送的应答丢失了,后续 A 发送的数据到了 B,那么 B 就会认为已经建立了连接;如果 B 出现异常,那么 A 发送的数据会报错,说 B 是不可达的,A就知道了 B 出现了异常,就会做出相应的处理。

至此 TCP 三次握手过程大致梳理清楚了。

TCP 数据包的序号问题

TCP 三次握手处理建立连接外,主要还是为了沟通一件事情: TCP 包的序号问题。

A 需要告诉 B 自己发送的包的序号的起始是从哪个号开始的,同理 B 也要做相同的事情。

为什么序号不能都从 1 开始呢?

针对这个问题,我们来看一个场景:

A 发送 1、2、3 包给 B,1、2 安全可靠的到达 B,包 3 由于各种原因绕了原路,没有到达 B ,此时 A 短暂掉线,后重新连接,发送的数据序号从 1 开始,然后发送 2,至此该发送过程结束,但是绕路的 3 数据包有回来了,此时 B 认为它是一个正常包,于是产生了错误。

所以每个连接都要有不同的序号,这个序号的起始序号是随时间变化的,可以认为一个 32 位计数器,每 4ms 加 1,重复的序列号至少需要 4 h,此时由于 IP 包头中 TTL 的存在,绕路的包已经在该时间内就不复存在了。

正是因为 TCP 序号的机制,才保证了 TCP 连接过程中的保证顺序、不丢包的指标。

TCP 三次握手中的状态机

在建立 TCP 连接时,为了维护这个连接,双方需要维护一个状态机^1,在建立连接过程中,双方的状态变化时序图如下:

状态时序图

  1. Client 和 Server 均处于 CLOSED 状态,此时 Server 主动 监听某个端口号。
  2. Client 主动发起连接 SYN ,之后 Client 处于 SYN-SENT 状态。
  3. Server 收到 Client 发起的连接,返回 SYN,并且 ACK Client 的 SYN,之后 Server 进入到 SYN-RCVD 状态。
  4. Client 收到 Server 发送的 SYN 和 ACK,发送 ACK 的 ACK ,之后 Client 进入 ESTABLISHED 状态,因为它一发一收成功了。
  5. Server收到 ACK 的ACK 后,进入 ESTABLISHED 状态,因为它一发一收成功了。

TCP 四次挥手

使用上面同样的例子,A、B 相同认识,进行简单的交流后,挥手说再见,各回各家。

1、A (A -> B): B ,我不能和你再聊了,我要回家了。
2、B (B -> A): 好的 A,我知道了。

此时只是 A 单方面的不想再聊,即 A 在此后不会发送数据了。但是 B 不能再应答 A 之后马上就关闭连接,为什么呢?因为在此状况下,只是 A 在发送数据后不再发送数据了,B 还有自己没有做完的事情,此时 B 还是可以发送数据的,此时处于 半关闭状态

此时 A 可以选择不再接收数据,也可以选择最后在接收一段数据,等待 B 也主动关闭。

3、B (B -> A): A 我也不玩了,Bye。
4、A (A -> B): 好的,Bye。

这样整个连接就关闭了,如建立连接时存在异常,同样关闭连接也会存在异常,如下:

异常情况

A 在说完“我不和你聊天了”,直接跑路,这是是会出现问题的,因为 B 还没发起结束,就算发起了,也得不到回答,此时 B 不知道怎么办。

还有一种异常情况是,当 A 说完 “我不和你聊天了, B 直接跑路,A 的发送的数据包没有得到应答,它不知道 B 是在处理自己的事情还是等一会发送结束。

TCP 四次挥手的时序图

四次挥手时序图

  1. Clinet 自己断开连接,发送 FIN 后进入 FIN-WAIT-1 状态。

  2. Server 收到 Client 的 FIN 请求,发送 FIN 的 ACK ,Server 进入 CLOSED-WAIT 的状态。

  3. Client 收到 Server 的 ACK 后进入 FIN-WAIT-2 的状态。

    若这个时候 Server 跑路,那么 A 将永远处在 FIN-WAIT-2 状态,TCP 协议没有对这个状态处理,但是 Linux 可以调整 tcp_fin_timeout 参数,来设置超时时间。

  4. Server 发送 FIN 的请求到达 Client , Client 发送该请求的 ACK ,然后结束了 FIN-WAIT-2 状态,按理说 Client 可以跑路了,但是万一自己发送的 ACK 没有成功到达 Server 怎么办?如果是这样的情况,Server 在一段时间后会重新发送 FIN 请求,但是 Client 已经跑路了,Server 永远也收不到 ACK 了。基于此原因 TCP 协议要求 Cleint 在最后需要等待一段时间 TIME-WAIT ,这个时间需要足够长,长到下面的步骤能够成功执行。

    如果 Server 超过了 2MSL 依然没有收到 ACK ,虽然 Server 会重发 FIN ,但是 Client 收到这个数据包后表示我已经等待了这么长时间,也够意思了,朋友再见,发送 RST 请求,Server 收到这个请求后就知道 Client 已经跑路了。

MSL(Maximum Segment Lifetime),报文最大生存时间,它是报文在网络中存在的最大时间,超过这个时间报文会被丢弃。

IP 头中有 TTL域,IP 数据包每经过一个处理它的路由器后,该值减 1,此值为 0 是数据包被丢且。

异常情况一

A 发送: ”不玩了“
B 回复: “知道了”

在这个回合中是不存在异常的,如果 A 发送的消息没有得到 B 的回复会重复发送。

但是在这个回合结束后,就有可能发生异常情况,因为存在一方先 “跑路” 的情况。

异常情况二

A 发起结束后,马上跑路,此时 B 发起的结束,是得不到 A 的回复的。

异常情况三

A 发起结束后,B 马上跑路,这时 A 不知道 B 的具体状态,是还有事情处理还是一会才会发送结束。