TOC

TCP (1): 连接的建立和关闭

TCP 报文

sequence number: 客户端随机创建一个 32 位初始序列号(ISN),然后对数据逐一编号。
acknowledgment num: 服务器端也随机创建 32 位序列号,用于确认。

  1. 来源端口,目的端口各 2Bytes
  2. 序列号和确认号各 4Bytes
  3. 两个字节:offset 4Bits + reverse 3Bits + flags 9Bits
  4. 窗口大小 Window Size: 2Bytes
  5. 校验和 Checksum: 2Bytes
  6. 紧急指针?Urgent Point: 2Bytes
  7. 可选字段,不超过 40Bytes

九个标记位

清楚上面 5 个应该就够了。

  • FIN Finish
  • SYN Synchronize 同步,应该是协商,交换序列号,或者说同步序列号
  • RST Reset
  • PSH Push
  • ACK Acknowledgement
  • URG Urgent
  • ECE Explicit Congestion Notification [ECN]-Echo
  • CWR Congestion Windows Reduced
  • NS Explicit Congestion Notification [ECN]-Nonce

TCP 连接状态

  • LISTEN server-only, 等待连接
  • SYN-SENT client-only
  • SYN-RCVD server-only
  • ESTABLISHED 连接建立
  • FIN-WAIT-1 主动关闭端发出 FIN 之后
  • FIN-WAIT-2 主动关闭端接收到 ACK 之后。半关闭,可以接收数据,但是不发送数据
  • CLOSE-WAIT 被动关闭端接收到 FIN 之后,立即回复 ACK。半关闭,可以发送数据,但不接收数据。
  • CLOSING 发出 FIN 包之后接收到 FIN 包
  • LAST-ACK 第三次挥手之后,即被动关闭方发出 FIN 之后,等待最后的 ACK
  • TIME-WAIT 第四次挥手之后,等待 2MSL 才关闭连接
  • CLOSED 连接关闭

三次握手 connect

Three Handshake

Connection Establishment

  • 第一次握手:客户端 syn, 带上 ISN_C
  • 第二次握手:服务器端 syn + ack, 带上 ISN_S
    同时将 ISN_C 用作 ACK_S
  • 第三次握手:客户端 ack
    同时将 ISN_S 用户 ACK_C

目的:协商通讯序列号范围(安全保障,范围错误的包会被丢弃)

为了协商(“同步”序列号),3 是最小的一个通讯次数。
这个过程也顺便确保了双方收发是通畅的。

  1. 客户端 SYN-SENT, 服务器端 SYN-RCVD 就是半连接状态
  2. 如果客户端的 SYN 没有到服务器,客户端会自动周期性超时重传
  3. 如果服务器端的 SYN + ACK 丢了,服务器会自动周期性超时重传
  4. 如果客户端 ACK 丢了,此时客户端进入了 ESTABLISHED 状态,并以为服务器端也一样
  5. 服务器端的超时重传
  6. 客户端发送数据,ACK 标识也有,服务器端进入 ESTABLISHED 状态
  7. 第三次握手携带数据是没有问题的,如果前两次握手携带数据不被限制就很容易被用于攻击
  8. SYN 攻击:又叫泛洪,恶意的客户端发送大量伪造的 SYN 包导致服务器一直在超时重发,未连接队列被挤爆,正常 SYN 包无法处理而被丢弃以致无法正常服务。(办法:缩短 SYN Timeout,增加最大半连接数,过滤网关防护,SYN cookies)
  9. 发出第一个 SYN 之后,可能接收到 ICMP error: destination unreachable
netstat -n -p TCP | grep SYN_RECV

同时连接的情况

Simultaneous Open

Simultaneous Open

四次挥手 close

Four Waves

Connection Termination

注意:这个图片是有问题的,服务器端也可以主动关闭连接,所以上面应该是发起人和接收人才对,维基百科配图上写的 Initiator 和 Receiver,我就叫它主动方和被动方。

  • 第一次挥手: 主动方: FIN “我完事了”
  • 第二次挥手: 被动方: ACK “我知道了” (继续做手头的事情)
  • 第三次挥手: 被动方: FIN + ACK “我也完事了,拜拜”
  • 第四次挥手: 主动方: ACK “拜拜”

目的:

  1. 为什么不像三次握手一样,需要被动方两次操作?为了被动方可以继续处理没有发送的数据。
  2. 一个接收到 FIN 包的连接(即被动方,处于 CLOSE-WAIT),又有数据包来了(非 ACK),会怎么处理?
    应该会被抛弃。
    如果最后的 ACK 带有数据呢?
    应该会被忽略。
  3. 一个发出 FIN 包的连接(即主动方,处于 FIN-WAIT-1),在接收 ACK 包之前,接收到了数据包,会怎么处理。
    会正常处理到这个数据。

同时关闭的情况

Simultaneous Close

Simultaneous Close

  • 接收到 FIN 之前,两端都处于 FIN-WAIT-1 状态,
  • 接收到 FIN 之后,两端都进入 CLOSING 状态,
  • 然后发出 ACK 包,然后都处于 TIME-WAIT 状态,
  • 等待 2MSL 之后,关闭连接

根据 RFC 793,一个连接可以在 TIME-WAIT (即主动断开的一方) 等待两个 MSL(最大报文寿命, Maximum Segment Lifetime), 一个 MSL 为 2 分钟。
目的是:1. 为了保证对方确实收到了 ACK,有足够时间重传 FIN-ACK。2. 也是保证对方的报文全部被消化了(可能有部分报文延迟,包括 SYN 包)

我做了关闭连接的时候,观察 ss -ant | grep TIME && date,发现我的 Ubuntu 20.04 上是大概一分钟,正好和 net.ipv4.tcp_fin_timeout 对的上,可能就是这个东西吧(看有些资料说是管 FIN_WAIT_2 的)。
PS: 服务器上配置的是 1

三次挥手的情况

被动关闭方直接返回 FIN + ACK,跳过半关闭状态。
我做了一个简单的小实验,然后发现:客户端中断连接之后,如果服务器也马上中断连接,FIN 会和 ACK 一起发,如果中间有一个中断,哪怕是 time.sleep(0),就交出 CPU 一下,FIN 就和 ACK 分开了。
我猜,应该是封装在操作系统 Socket 实现中的支持,可能系统有一个队列,发现有可以一起发送的就合并在一起。

                                                        C E U A P R S F
                                                        W C R C S S Y I
                                                        R E G K H T N N
13:24:46.294002 127.0.0.1:39114 -> 127.0.0.1:8000  [0 0 0 0 0 0 0 0 1 0] b''  # 握手
13:24:46.294016 127.0.0.1:8000  -> 127.0.0.1:39114 [0 0 0 0 0 1 0 0 1 0] b''
13:24:46.294027 127.0.0.1:39114 -> 127.0.0.1:8000  [0 0 0 0 0 1 0 0 0 0] b''
13:24:46.294346 127.0.0.1:8000  -> 127.0.0.1:39114 [0 0 0 0 0 1 1 0 0 0] b'welcome'
13:24:46.294357 127.0.0.1:39114 -> 127.0.0.1:8000  [0 0 0 0 0 1 0 0 0 0] b''
13:24:46.796128 127.0.0.1:39114 -> 127.0.0.1:8000  [0 0 0 0 0 1 0 0 0 1] b''  # 挥手
13:24:46.796489 127.0.0.1:8000  -> 127.0.0.1:39114 [0 0 0 0 0 1 0 0 0 1] b''
13:24:46.796538 127.0.0.1:39114 -> 127.0.0.1:8000  [0 0 0 0 0 1 0 0 0 0] b''

参考资料与拓展阅读