TCP建立连接为什么是三次握手,为什么不是两次或四次?

TCP三次握手,四次挥手 - 图1

  • 第一次握手:客户端发送TCP包,置SYN标志位为1,将初始序号X,保存在包头的序列号(Seq)里。
  • 第二次握手:服务端回应确认包,置SYN标志位为1,置ACK为X+1,将初始序列号Y,保存在包头的序列号里。
  • 第三次握手:客户端对服务端的确认包进行确认,置SYN标志位为0,置ACK为Y+1,置序列号为Z。

为什么不是两次

重新看一遍图(S-服务端,C-客户端)

  • 第一次握手后,S可以确认自己收报文与C发报文的功能都正常,而C呢,它什么都不能确认。
  • 第二次握手后,C可以确认自己的收发报文与S的收发报文功能都正常,也就是认为连接已建立。
  • 那么第三次呢,S也可以确认双方能够正常通信。

假想一下,如果我们去掉了第三次呢?
如果只是第二次建立的话,服务端和客户端就已经建立,但是如果客户端没有收到服务端的回应?这个时候,客户端认为没有建立,服务端却为认为建立成功,并保存了必要的资源,如果出现大量的这样的情况。那么服务器会奔溃。
因此第三次握手是必要的。

为什么不是四次

因为三次握手后,C和S至少可以确认之前的通信情况,但无法确认之后的情况。 所以如果四次还是五次或是更多次都是徒劳的。

TCP(七) — 四次挥手

一:摘要概述

经历三次握手,MTU与MSS对数据包大小限制,滑动窗口对于发送端流量控制,拥塞控制对网络状态的控制,以及三次握手过程中的连接队列详解。

二:四次挥手过程

下面过程描述中将主动关闭方定义为A、被动关闭方定义为B
TCP三次握手,四次挥手 - 图2

  • A发送FIN包申请关闭连接,此时A进入FIN-WAIT-1状态,不会再向B端发送数据包
  • B接收到FIN包后会立即回复ACK包,此时B进行CLOSE-WAIT状态
  • A接收到B传输的ACK确认包后会将其状态修改为FIN-WAIT-2
  • B检查自身是否还有数据需要发送,如果有将会发送完所有未发送数据,若无则发送FIN包进入LAST-ACK状态
  • A接收到B的FIN包后会发送ACK包,进入TIME-WAIT状态等待
  • 两个MSL时钟后A将会释放连接CLOSED
  • B收到A传输的ACK确认包后将会直接释放连接进入CLOSED状态

    三:四次挥手模拟

    1. --tolerance_usecs=1000000
    2. 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
    3. +0 bind(3, ..., ...) = 0
    4. +0 listen(3, 1) = 0
    5. +0 < S 0:0(0) win 65535 <mss 100>
    6. +0 > S. 0:0(0) ack 1 <...>
    7. .1 < . 1:1(0) ack 1 win 65535
    8. +.1 accept(3, ..., ...) = 4
    9. // 服务端主动断开连接
    10. +.1 close(4) = 0
    11. +0 > F. 1:1(0) ack 1 <...>
    12. // 向协议栈注入 ACK 包,模拟客户端发送了 ACK
    13. +.1 < . 1:1(0) ack 2 win 257
    14. // 向协议栈注入 FIN,模拟服务端收到了 FIN
    15. +.1 < F. 1:1(0) win 65535 <mss 100>
    16. +0 `sleep 1000000`
    17. 复制代码

    TCP三次握手,四次挥手 - 图3可以看到图中红框部分,其过程如下:

  • 服务端发送FIN包断开连接

  • 客户端回复ACK
  • 客户端再次回复FIN
  • 服务端回复ACK

    四:四次次挥次数

    四次挥手一定是四次?这是经常会出现的灵魂拷问,你会说一大堆理由支撑这个观点。其实最主要还是在于被动关闭方需要检查是否还有数据需要进行传输,所以将FIN与ACK进行了两次发包。这就比三次握手多了一环,回想三次握手时SYN+ACK是一个包进行发送。但是这个思考一下,如果没有数据发送的时候会不会将FIN+ACK包一起进行发送?看如下截图验证:
    TCP三次握手,四次挥手 - 图4上一篇文章刚讲到了延迟确认的机制,请仔细回忆。TCP(六) — 重传与确认

    五:TIME-WAIT状态

    主动关闭方往往需要等待2MSL时间才能关闭进入CLOSED状态,那么问题来了,等待2MSL时间的TIME-WAIT状态到底有什么用?为什么会这么设计?
    TCP三次握手,四次挥手 - 图5当存在网络延迟的时候往往可能就会有姗姗来迟,如上图所示。如果这时候立即进入CLOSED状态,且正好有连接使用的四元组件相同,就会导致新的连接接收到旧连接的数据包。总得来讲就是让旧连接数据包消失在网络传输中TCP三次握手,四次挥手 - 图6
    除了消除掉网络中旧连接的数据包之外,还有一个原因就是充分保证ACK包的可达。当ACK包丢失时若接收到服务端超时重传的FIN包可以回复ACK包使得服务端进入CLOSED状态

    六:timestamp

    TCP三次握手,四次挥手 - 图7请求时间戳,这是一个增量的数据,与系统时间戳并没有关系。需要客户端与服务端同时打开才会生效,打开参数位置位于/proc/sys/net/ipv4/tcp_timestamp,值为1是打开状态、0关闭TCP三次握手,四次挥手 - 图8

  • TSval存储本次请求的增量时间戳

  • Tsecr存储数据发送方上次的时间戳

    七:tcp_tw_reuse

    当系统中存在大量TIME_WAIT状态连接时无疑是一种极其严重的浪费行为,所以对于TIME-WAIT状态的连接希望可以进行复用。系统中提供了参数/proc/sys/net/ipv4/tcp_tw_reuse参数,可以对TIME-WAIT的参数进行重复使用,其原理如下所示:
    TCP三次握手,四次挥手 - 图9

  • 如果有网络波动导致的旧连接包迷路,新连接判断到该包携带的时间戳小于当前记录时间戳时就会将其抛弃

  • 如果因为ACK丢包,被动关闭方重传FIN+ACK时,主动关闭方也使用时间戳判断到该现象即可回复RST断开连接

相对于参数tcp_tw_reuse来讲还有一个更加激进的参数tcp_tw_recyle,该参数会缓存所有IP创建连接的时间戳,会丢弃调所有比该时间戳小的数据包。这样的策略在NAT网络中就是爆炸性的操作,所以这个参数都是直接将其关闭

八:SO_REUSEADDR

经常会发现一个问题,当服务端程序崩溃的时候重启服务发现提示你该地址已经被占用,what?其实这时候就是TIME-WAIT在搞鬼。如果程序崩溃重启要等待2MSL时间,这肯定是不合理的,所以可以设置参数SO_REUSEADDR,允许对处于TIME-WAIT状态连接的端口进行绑定。这个参数一般会在服务端进行设置,因为客户端的连接端口基本都是动态的,但是服务端都是固定监听某个端口

九:SO_LINGER

TCP三次握手,四次挥手 - 图10当关闭连接时默认的都是发送完所有数据缓冲区的数据后才发送FIN包进行四次挥手断开连接,但是如果不想等待这么久直接关闭连接应该怎么办?系统提供SO_LINGER参数,该参数开启后提供两个参数,如下所示:

  • l_onoff: 0表示禁用,非0表示启动SO_LINGER
  • l_linger:当l_onoff参数非0表示启用时。该参数为0表示直接丢弃所有缓冲区数据,并发送RST断开连接。非0表示给与数值时间,时间内不论是否发送完数据都将发送RST包断开连接