TCP 报文
sequence number: 客户端随机创建一个 32 位初始序列号(ISN),然后对数据逐一编号。
acknowledgment num: 服务器端也随机创建 32 位序列号,用于确认。
- 来源端口,目的端口各 2Bytes
- 序列号和确认号各 4Bytes
- 两个字节:offset 4Bits + reverse 3Bits + flags 9Bits
- 窗口大小 Window Size: 2Bytes
- 校验和 Checksum: 2Bytes
- 紧急指针?Urgent Point: 2Bytes
- 可选字段,不超过 40Bytes
九个标记位
清楚上面 5 个应该就够了。
FIN
FinishSYN
Synchronize 同步,应该是协商,交换序列号,或者说同步序列号RST
ResetPSH
PushACK
AcknowledgementURG
UrgentECE
Explicit Congestion Notification [ECN]-EchoCWR
Congestion Windows ReducedNS
Explicit Congestion Notification [ECN]-Nonce
TCP 连接状态
LISTEN
server-only, 等待连接SYN-SENT
client-onlySYN-RCVD
server-onlyESTABLISHED
连接建立FIN-WAIT-1
主动关闭端发出 FIN 之后FIN-WAIT-2
主动关闭端接收到 ACK 之后。半关闭,可以接收数据,但是不发送数据CLOSE-WAIT
被动关闭端接收到 FIN 之后,立即回复 ACK。半关闭,可以发送数据,但不接收数据。CLOSING
发出 FIN 包之后接收到 FIN 包LAST-ACK
第三次挥手之后,即被动关闭方发出 FIN 之后,等待最后的 ACKTIME-WAIT
第四次挥手之后,等待 2MSL 才关闭连接CLOSED
连接关闭
三次握手 connect
Three Handshake
- 第一次握手:客户端 syn, 带上 ISN_C
- 第二次握手:服务器端 syn + ack, 带上 ISN_S
同时将 ISN_C 用作 ACK_S - 第三次握手:客户端 ack
同时将 ISN_S 用户 ACK_C
目的:协商通讯序列号范围(安全保障,范围错误的包会被丢弃)
为了协商(“同步” 序列号),3 是最小的一个通讯次数。
这个过程也顺便确保了双方收发是通畅的。
- 客户端 SYN-SENT, 服务器端 SYN-RCVD 就是半连接状态
- 如果客户端的 SYN 没有到服务器,客户端会自动周期性超时重传
- 如果服务器端的 SYN + ACK 丢了,服务器会自动周期性超时重传
- 如果客户端 ACK 丢了,此时客户端进入了 ESTABLISHED 状态,并以为服务器端也一样
- 服务器端的超时重传
- 客户端发送数据,ACK 标识也有,服务器端进入 ESTABLISHED 状态
- 第三次握手携带数据是没有问题的,如果前两次握手携带数据不被限制就很容易被用于攻击
- SYN 攻击:又叫泛洪,恶意的客户端发送大量伪造的 SYN 包导致服务器一直在超时重发,未连接队列被挤爆,正常 SYN 包无法处理而被丢弃以致无法正常服务。(办法:缩短 SYN Timeout,增加最大半连接数,过滤网关防护,SYN cookies)
- 发出第一个 SYN 之后,可能接收到 ICMP error: destination unreachable
netstat -n -p TCP | grep SYN_RECV
同时连接的情况
Simultaneous Open
四次挥手 close
Four Waves
注意:这个图片是有问题的,服务器端也可以主动关闭连接,所以上面应该是发起人和接收人才对,维基百科配图上写的 Initiator 和 Receiver,我就叫它主动方和被动方。
- 第一次挥手: 主动方: FIN “我完事了”
- 第二次挥手: 被动方: ACK “我知道了” (继续做手头的事情)
- 第三次挥手: 被动方: FIN + ACK “我也完事了,拜拜”
- 第四次挥手: 主动方: ACK “拜拜”
目的:
- 为什么不像三次握手一样,需要被动方两次操作?为了被动方可以继续处理没有发送的数据。
- 一个接收到 FIN 包的连接(即被动方,处于 CLOSE-WAIT),又有数据包来了(非 ACK),会怎么处理?
应该会被抛弃。
如果最后的 ACK 带有数据呢?
应该会被忽略。 - 一个发出 FIN 包的连接(即主动方,处于 FIN-WAIT-1),在接收 ACK 包之前,接收到了数据包,会怎么处理。
会正常处理到这个数据。
同时关闭的情况
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''
参考资料与拓展阅读
- https://en.wikipedia.org/wiki/Transmission_Control_Protocol
- https://zh.wikipedia.org/zh/传输控制协议
- TCP的三次握手及四次挥手详解
- 知乎,TCP 为什么是三次握手,而不是两次或四次? - 车小胖的回答
- TCP State Transitions
- https://github.com/steveLauwh/TCP-IP/blob/master/TCP/Open%20and%20Closed%20at%20the%20same%20time.md
- Shichao's Notes, Chapter 13. TCP Connection Management
- Oracle, Configuring FIN_WAIT_2 timeout on Linux
- IBM, TCPIP IPv4 settings
- RedHat, Changing tcp_fin_timeout and tcp_max_tw_buckets
- TCP:关于FIN_WAIT_2、TIME_WAIT和CLOSE_WAIT
- What is Tcp_fin_timeout?
- 微信公众号, 水滴与银弹, 深度剖析TCP三次握手,面试官拍案叫绝!
- 微信公众号, 网络技术平台, 28 张图,搞懂TCP
- 微信公众号, 网络技术平台, TCP 重传、滑动窗口、流量控制、拥塞控好难?看完图解就不愁了