文章分为核心前置知识、三次握手、四次挥手以及相关思考和问题感兴趣的可以全部读一读一、核心前置知识TCP 报文的“密码”在看握手和挥手之前必须先认得 TCP 报文头部的几个控制位Flags。它们就像是旗语只有 0关和 1开两种状态1. SYN (Synchronize) “同步”信号灯这盏灯专门用来建立连接。它亮起是为了告诉对方“我想和你对一下暗号同步初始序列号 seq。”SYN 1亮 “我想和你建立新连接”。整个 TCP 通信生命周期中只有在三次握手的前两次客户端发起请求、服务端同意请求并反向请求这盏灯才会亮起。SYN 0灭 “这是一封普通的、处于通信中的信”。只要连接一旦建立成功第三次握手及以后这盏灯就会永久熄灭再也不会亮起。2. ACK (Acknowledge) —— “收到确认”信号灯这盏灯是 TCP 可靠传输的灵魂用来给对方报平安。ACK 1亮 “我这封信里的确认号 ack 是有效的我收到了你之前发的数据。”TCP 规定除了客户端发出的第一封信第一次握手以外后面所有人发出的所有报文ACK 都必须是 1。只要连接成了大家说话都得带着“收到”的标志。ACK 0灭 “这封信里的确认号 ack 无效忽略它。”这种情况极其罕见只有在第一次握手时客户端单方面发起连接此时它还没收到服务端的任何回应所以 ack 没意义此时 ACK 0。3. FIN (Finish) —— “再见/结束”信号灯这盏灯专门用来拆除连接。它亮起代表一方要开始“撂电话”了。FIN 1亮 “我的应用数据已经全部发完了我要关闭我这边的发送通道了。”在四次挥手时客户端想断开会发一封 FIN1 的信服务端想断开也会发一封 FIN1 的信。它亮起就意味着“再见”。FIN 0灭 “我还在正常传输数据呢没打算断开。”在平时的正常通信、以及建立连接的三次握手期间这盏灯都是灭的。如果把 TCP 通信比作两个人在通过挂号信写一本连载小说那么 seq 和 ack 就是书页里的“字符计数器”。因为 TCP 是全双工双向通信所以每个人手里都有一个独立的计数器。4. seq序号当前发送的“起点位置”TCP 极其严谨它不按“第几条消息”来算账而是按“字节Byte”来算账。只要连接一建立TCP 就会为你要发送的每一个字节的数据在心里编一个号。seq 的含义就是告诉对方“这封信里装的数据是从我这边全局数据流的第几个字节开始的”。5. ack确认号期望下次收到的“起点位置”ack 则是用来向对方“催更”或“报平安”的。ack 的含义告诉对方“到第 X-1 个字节为止的数据我全都稳稳收到了你下一封信请从第 X 个字节开始发给我。”这里为什么是 x - 1?注意这里是数据传输时的ack传输过程中是有一个实际的len数据长度的 如果对方这次的起点 seq 101数据长度 len 100。 对方发过来的字节编号范围从 101 开始往后数 100 个最后一个字节的编号是 200。 公式计算 ackseq len $101 100 $ 201这就是 $X$。 代入大白话 “到第 $201 - 1 200$ 个字节为止的数据我全都收到了你下一封信请从第 201 个字节开始发。” 核心逻辑 因为 TCP 算的是“下一发数据的起始位置”。 ack 的数值$X$代表的是尚未收到的、排在最前面的那一个空位。既然它是第一个没收到的那它紧挨着的上一个位置$X - 1$自然就是最后一个已稳稳收到的。 三次握手时的 ack x 1,注意这里的1是默认长度不要和上面的x - 1联想到一起不一样的概念先了解下这个1具体三次握手过程我们看后面 核心特殊规则没有数据强行 1 在正常的通信中ack seq len数据长度。 但在三次握手和四次挥手时报文里是没有应用层数据的也就是说 len 0。 如果按照常规公式算ack seq 0 seq那对方下一次发过来的起点还是 seq这就原地打转了。 为了解决这个问题TCP 规定只要控制位里的 SYN同步或者 FIN结束标志灯亮起为 1这个报文就算没有任何数据也必须硬性、强行占掉 1 个字节的编号位置 因此三次握手时的计算公式变成了ack 对方的 seq 1。 所以ack 的数值永远是对方刚发过来的 seq 对方刚发过来的数据长度。二、上述知识了解后我们来看看三次握手1. 第一次握手客户端发起连接做什么客户端随机生成一个初始序号 seq x把控制位 SYN 设为 1向服务端发送首个报文。为什么没有 ack 因为这是第一封信客户端还没有收到过服务端的任何回应ACK 灯是灭的0此时的 ack 没有任何意义通常为 0。状态变化客户端由 CLOSED关闭状态进入 SYN_SENT同步已发送 状态。此时底层这个报文不能携带任何应用层数据如 HTTP 请求但它会消耗掉一个序号所以下一步对方回的 ack 会是 x 1。2. 第二次握手服务端响应并反向请求做什么服务端收到 SYN 报文后同意建立连接。它需要做两件事确认客户端的请求把 ACK 设为 1并将确认号设为 ack x 1。这里的 ack 计算公式对方的 seq 强行 1。大白话就是“客户端到第 x 个字节为止的‘建立连接暗号’我全都稳稳收到了你的诚意我懂了。接下来你再跟我说话第三回合请从第 x 1 个字节开始发给我”自己也发起连接请求随机生成自己的初始序号 seq y把 SYN 设为 1。这两个动作合并成一个 SYNACK 报文发回。状态变化服务端由 LISTEN监听进入 SYN_RCVD同步已接收 状态。3. 第三次握手客户端最后确认做什么客户端收到服务端的 SYNACK 报文。对服务端的连接请求进行确认把 ACK 设为 1确认号设为 ack y 1自己的序号变为 seq x 1。发送给服务端。这里的 ack 根据公式对方的 seq 强行 1。大白话“服务端你发过来的连接请求我也收到了到第 y 个字节为止的暗号我全拿到了你放心吧。咱们现在连接建好了你接下来如果要给我发真正的网页数据请从第 y 1 个字节开始发给我。”状态变化客户端发送后立刻进入 ESTABLISHED已建立连接 状态。服务端收到该报文后也进入 ESTABLISHED 状态。此时底层第三次握手的报文可以携带具体的应用层数据了。 三次握手时的 ack其本质就是“给对方的连接请求SYN打一个回执” 服务端回的 ack 101是对客户端 SYN (seq100) 的签收证明。 客户端回的 ack 501是对服务端 SYN (seq500) 的签收证明。 正因为双方都拿到了对方明确写着 1 的 ack 回执彼此才敢百分之百地确信“对方不仅听到了我的声音而且我们的序列号已经完美对齐了接下来可以放心大胆地传文件了” 深度思考为什么“两次”不行“四次”不多余吗 为什么不能是两次为了防止已失效的连接请求突然传到服务端引起错误 假设场景客户端发了第一个 SYN但在网络堵车了。客户端以为丢包了于是重发了第二个 SYN。这次网络很顺两次握手成功传完数据连接关闭了。 问题来了这时候那条堵车的第一个 SYN 终于开到了服务端。如果只要两次握手服务端就会认为客户端又发起了一个新连接于是进入 ESTABLISHED 状态在原地苦苦等待。但客户端实际上根本没想发数据服务端的资源就被白白浪费了。 有了第三次握手服务端收到迟到的 SYN 即使回了 SYNACK客户端发现自己没发过这个请求就不会理它服务端迟迟收不到第三次 ACK就会知道这个连接是无效的。 三、四次挥手TCP 的连接是双向全双工的断开时必须两端分别独立关闭。1. 第一次挥手客户端主动关闭发送做什么客户端不想发数据了发送一个 FIN 1 的报文随机生成序号 seq u。状态变化客户端进入 FIN_WAIT_1终止等待1 状态。注意此时客户端进入“半关闭”状态它不能再发送应用数据但如果服务端发来数据客户端依然可以接收。2. 第二次挥手服务端确认进入半关闭做什么服务端收到 FIN 报文发送一个 ACK 1 进行确认确认号 ack u 1带上自己的当前序号 seq v。状态变化服务端进入 CLOSE_WAIT关闭等待 状态。这时候应用层会被通知对方要断开了。客户端收到这个 ACK 后进入 FIN_WAIT_2终止等待2 状态。此时底层此时 TCP 连接处于半关闭Half-Close。客户端到服务端的这条路断了但服务端如果还有数据没发完可以继续往客户端发。3. 第三次挥手服务端数据发完申请关闭做什么服务端把最后缓存的数据全部发送完毕后也想关闭连接了。于是向客户端发送 FIN 1, ACK 1 的报文由于中间可能发了些数据它的当前序号变成了 seq w确认号依然保持 ack u 1。状态变化服务端进入 LAST_ACK最后确认 状态。4. 第四次挥手客户端最终确认做什么客户端收到服务端的 FIN 报文必须最后回复一个 ACK 1 确认ack w 1seq u 1。状态变化服务端收到该 ACK 后直接进入 CLOSED 状态服务端断开完成。客户端发送后进入 TIME_WAIT时间等待 状态。在这个状态死等 2MSLMaximum Segment Lifetime报文最大生存时间之后如果没有异常才最终进入 CLOSED 状态。四、思考和问题1. 为什么第四次挥手后客户端必须在 TIME_WAIT 状态等 2MSL为了保证最后一次 ACK 能够安全送达服务端网络是不可靠的。假设客户端发出的第四次 ACK 在路上丢包了服务端在 LAST_ACK 状态下迟迟收不到确认就会认为自己的第三次 FIN 丢了于是重发 FIN。如果客户端发完 ACK 就直接 CLOSED 了当服务端的重发 FIN 到达时客户端已经找不到对应的连接了就会回复一个 RST复位重置报文这会导致服务端报错通常是 Connection reset by peer无法优雅地正常关闭。客户端等 2MSL一去一回的最长时间就能保证如果服务端没收到能在该时间内重发客户端也能在这等它并重新补发 ACK。防止“已失效的连接请求报文”出现在新连接中等待 2MSL 的时间足以让本次连接在网络中产生的所有残留报文比如在某个路由器里堵车延迟的报文全部死掉并消失。这样当下次你再用相同的 IP 和端口建立新连接时就绝不会收到上一次连接遗留下来的脏数据2. 什么是 已失效的连接请求报文假设你在家里用电脑连接服务器整个故事分为前世和今生前世上一次连接客户端发送了一个连接请求SYN 报文假设它的序列号 seq 100。结果这个报文走到半路网络大堵车它被卡在某个慢吞吞的路由器缓存里了。客户端等了半天没回应以为丢包了于是重新发了一个 SYNseq 200。这次很顺利三次握手成功数据传完了双方四次挥手正常断开。重点来了 那个卡在半路的第一个 SYNseq 100依然活着它在网络里继续晃悠。这个报文就叫做“已失效的连接请求报文”。今生紧接着发起的新连接如果挥手后不等待会发生什么恐怖的事上一次连接断开后你马上又用相同的 IP 和相同的端口号向同一个服务器发起了新连接。就在新连接刚刚建立好双方准备传数据的时候那个在网络里堵车很久的、前世遗留下来的 SYNseq 100突然满血复活终于开到了服务器如果四次挥手时客户端没有 TIME_WAIT 的限制而是发完最后一个 ACK 立刻彻底关闭那么服务器看到这个延迟大半天的 SYN误以为是客户端又发起了一次全新的请求。服务器就会一本正经地回复 SYN ACK。此时客户端会莫名其妙地收到一个来自前世的确认报文导致整个 TCP 的状态机彻底陷入混乱甚至可能把前世没发完的旧数据混进今生的新数据里导致传输的文件损坏3. 四次挥手时的 TIME_WAIT 是如何解决这个问题的为了消灭这种“跨时空污染”TCP 规定在四次挥手的最后一步客户端发送完最后一个 ACK 后连接并没有真正关闭而是把端口锁死进入 TIME_WAIT 状态死等 2MSL 时间。MSLMaximum Segment Lifetime是一个报文在网络里能活下来的最大时间。如果超过这个时间网络里的路由器就会无情地把它丢弃。在现代 Linux 操作系统中默认的 MSL 通常被硬编码设置为 30 秒所以 2MSL 就是 60 秒。在老旧的系统或标准的 TCP 规范RFC 793中官方建议的 MSL 甚至是 2 分钟2MSL 就是 4 分钟。为什么要等 2 倍2MSL 因为 1 个 MSL 保证客户端发出的最后一个 ACK 能到达对方万一这个 ACK 丢了服务端重发的 FIN 传回来又需要 1 个 MSL。这一来一回刚好是 2MSL。4. 2MSL 真正等的是谁TIME_WAIT 的 2MSL 时间是从第四次挥手客户端发送最后一个 ACK的那一刻才开始倒计时的。注意这里2msl只是从第四次挥手那一刻开始倒计时并不只是为了熬死第四次发出去后失效的报文而是为了熬死所有属于这次连接的、活着的报文它等的是在挥手前后这一小段时间内网络里可能刚刚产生的、还在赶路的、属于本次连接的所有残余报文。也就是说TCP 强制全员在原地罚站 2MSL就是为了保证此时此刻网络中所有属于这次连接的、活着的报文全部超过 1个 MSL 的寿命而死绝。5. 从第四次挥手后有多个丢失的报文 2MSL 够用吗重传机制和最大重传次数限制绝对够用。不管丢了多少个报文2MSL 都是理论和实际上的“终极安全边界”。这里的核心秘密在于2MSL 的倒计时并不是一成不变的它是可以被“无限续期刷新”的。场景推演如果第四次挥手的 ACK 丢了假设客户端发出第四次挥手的 ACK并且网络里还有其他残留报文在乱飞客户端 发出最后一个 ACK开始 2MSL 倒计时假设一共要等 2 分钟。网络中 这个 ACK 走到半路丢了。服务端 因为没收到 ACK触发了超时重传机制。服务端会认为“我刚才发的第三次挥手 FIN 是不是丢了” 于是服务端重新发送了一个 FIN。客户端 还在 2MSL 的“原地罚站”状态中。突然它又收到了服务端重传的 FIN。【关键机制触发】 客户端只要在 TIME_WAIT 期间收到任何来自对方的旧报文比如重传的 FIN它就会重新发送一次 ACK并且把 2MSL 的定时器直接清零重新开始倒计时 2 分钟为什么说不管丢多少个报文都够用通过上面的重传机制你会发现 2MSL 的真正威力在于只要网络里还有“活着的旧报文”折腾 服务端就会因为收不到确认而不断重传 FIN。客户端每收到一次 FIN就会续命刷新一次 2MSL。直到什么时候才停止 直到某一时刻客户端发出的 ACK 终于成功送达服务端服务端彻底关闭CLOSED不再重传了或者服务端重传达到最大次数强制断开。最后的平静 当网络里最后一个重传的 FIN 报文传到客户端或者在路上死掉之后网络里就再也没有任何新的报文产生了。2MSL 开始最后的清空 客户端以这“最后一个报文出现的时间”为起点孤独地等待完整的 2MSL。1 个 MSL 保证客户端最后补发的那个 ACK 要么到了要么死在路上了。第 2 个 MSL 保证服务端如果没收到、再次重传的 FIN如果还有的话在通往客户端的路上彻底死掉。当这最后的 2MSL 顺利走完意味着全网所有可能在赶路的、重传的、迷路的、错乱的报文已经全部超过寿命上限无一幸免全部死绝了。总结2MSL 够用不是因为时间长而是因为 2MSL 永远是以“网络中最后一点风吹草动”为起点开始计算的。它是一座永远能帮网络“清空历史、重头再来”的终极安全防御塔。6. 为什么是2MSL 不是1MSL?等 1 个 MSL 只能保证“单向”的报文死绝而等 2 个 MSL才能保证“一去一回”双向的报文在全网彻底死绝。因为第四次挥手时客户端面临的不是一个平静的网络它刚刚发出了最后一个 ACK而服务端可能还在痴痴地等。为了搞懂为什么必须是 2 倍我们来推演一下如果只等 1 个 MSL网络会发生怎样惨烈的“连环撞车事件”致命推演为什么 1 个 MSL 绝对不够假设我们把时间卡在第 0 秒客户端向服务端发送了第四次挥手的最后一个 ACK。此时这个 ACK 在网络中最多能活 1 个 MSL。倒霉鬼场景第 0 秒 客户端发出最后的 ACK。第 0.9 个 MSL 的时候 这个 ACK 运不好走到半路丢包死掉了。服务端开始发飙 服务端在左等右等了将近 1 个 MSL 之后依然没有收到 ACK。由于服务端的超时重传机制它判定自己刚才发的第三次挥手 FIN 丢了。于是服务端在第 1 个 MSL 的瞬间重新打包并发送了一个新的 FIN 报文。客户端如果只等 1 个 MSL 就在服务端发出重传 FIN 的同一秒客户端的“1个MSL定时器”到期了。客户端开心地想“太好了1个MSL到了我自由了” 于是客户端瞬间关闭了端口CLOSED。灾难发生了服务端在第 1 个 MSL 时发出的那个重传的 FIN才刚刚出发啊这个全新的 FIN 报文在网络里又可以活 1 个 MSL。它晃晃悠悠地往客户端飞去。此时客户端已经关闭了或者更糟——客户端在第 1.001 个 MSL 的时候立刻用相同的 IP 和端口启动了一个新连接结果在第 1.5 个 MSL 的时候那个服务端重传的旧 FIN 突然拍在了客户端新连接的脸上。新连接瞬间风中凌乱直接崩溃。2MSL 的精妙之处闭环的时间对称性为了堵住上面这个由于“超时重传”导致的致命时间差计算机科学家设计了 2个 MSL。这 2个 MSL 的分工极其明确刚好对应一去一回的极限时间第 1 个 MSL“去”的极限用来等待客户端发出的最后一个 ACK 走到服务端。如果这个 ACK 顺利走完 1 个 MSL 还没到说明它在路上死了。第 2 个 MSL“回”的极限如果 ACK 死了服务端会在最极限的时刻大约第 1 个 MSL 结束时触发重传把旧的 FIN 再次拍出来。这个新出来的 FIN 飞回客户端又最多需要 1 个 MSL 的时间。总结客户端在第四次挥手后老老实实原地罚站 2MSL本质上是在用时间做一堵双向防火墙如果网络好ACK 在第 1 个 MSL 内到了服务端安静关闭。剩下的 1 个 MSL 时间里网络一片寂静客户端安全退出。如果网络坏ACK 丢了服务端重传的 FIN 必然会在 2MSL 期间传到客户端。客户端一旦收到就会立刻重发 ACK 并刷新重置2MSL 定时器重新再等 2 分钟直到网络彻底干净为止。所以1 个 MSL 只能保证自己发的信息死透2 个 MSL 才能保证对方因为没收到而产生的“报复性重传”也死透。 TCP 用这个天才的对称性设计换来了互联网几十年的稳定。7. 为什么这2MSL等待不在服务端 而在客户端?这里有两个最核心的底层设计考量“谁主动谁负责” 的原则以及对 服务器资源的保护。核心铁律谁主动关闭2MSL 就在谁身上其实TCP 协议并没有规定 TIME_WAIT 必须在“客户端”。它的准确规定是谁先发起主动关闭Active Close2MSL 的等待就由谁来承担。在绝大多数网络场景下比如你用浏览器访问网站都是客户端传完数据、看完网页后主动划掉页面或者断开连接。因为是客户端主动发起的断开所以客户端承担了这 2MSL 的等待。如果网站服务器因为某些原因比如超时、或者垃圾清理主动把你的连接断开了那这个 2MSL 的 TIME_WAIT 状态就会留给服务器。为什么要把这个负担“尽量”压在客户端身上原因一服务器的端口资源极其宝贵怕端口耗尽客户端的视角 客户端作为一台个人电脑同一时间可能也就建立几十个、几百个网络连接。让它拿出一个端口原地罚站 2 分钟无伤大雅用户根本感知不到。服务端的视角 服务器比如一台高并发的 Web 服务器每秒钟可能要处理几万个用户的连接。如果每个用户离开后服务器都要为这个连接默默死等 2 分钟那么短时间内服务器上就会堆积几十万个处于 TIME_WAIT 状态的死连接。后果 操作系统的端口号和文件描述符是有限的通常一个 IP 最多 65535 个端口。如果全部被 TIME_WAIT 占满了服务器就会陷入“端口耗尽”的窘境再也无法接收任何新用户的请求了。原因二符合“保全大局”的设计哲学在网络架构中有一个核心原则叫“把负担和状态尽量推向网络的边缘客户端保证网络核心服务端的高效和轻量。”客户端崩了只影响这一个小明但如果服务器因为端口被死等的连接占满而崩了全天下几万个小明、小红都无法访问网站了。所以这个“负责收尾清空幽灵报文”的安全重任自然要由客户端来扛。如果服务端不得不“主动关闭”怎么办在实际开发中有时候服务器必须主动断开不听话的客户端比如客户端恶意占用连接不发数据。这时服务器就会不可避免地进入 TIME_WAIT 状态。为了防止上面说的“服务器端口耗尽”导致瘫痪现代网络编程和操作系统提供了几种高级外挂SO_REUSEADDR端口复用这是后端开发中最常用的套接字选项。它告诉操作系统“就算这个端口现在处于 TIME_WAIT 状态只要新来的连接序列号对得上允许新连接直接抢用这个端口”直接打破 2MSL 的死等限制。发出 RST 报文直接闪退服务器如果想彻底甩开 2MSL 的纠缠可以直接向客户端发送一个 RST复位报文而不是走正常的四次挥手。这相当于粗暴地直接挂断电话不触发四次挥手也就完全不会产生 TIME_WAIT 状态。总结2MSL 留给客户端是因为客户端往往是主动说再见的那个人。TCP 协议让“主动说再见的人负责留下来打扫战场”既能保证网络数据的绝对安全又能完美保护服务器不被海量的死连接活活拖垮。
借助AI再次理解三次握手和四次挥手
发布时间:2026/6/10 15:10:40
文章分为核心前置知识、三次握手、四次挥手以及相关思考和问题感兴趣的可以全部读一读一、核心前置知识TCP 报文的“密码”在看握手和挥手之前必须先认得 TCP 报文头部的几个控制位Flags。它们就像是旗语只有 0关和 1开两种状态1. SYN (Synchronize) “同步”信号灯这盏灯专门用来建立连接。它亮起是为了告诉对方“我想和你对一下暗号同步初始序列号 seq。”SYN 1亮 “我想和你建立新连接”。整个 TCP 通信生命周期中只有在三次握手的前两次客户端发起请求、服务端同意请求并反向请求这盏灯才会亮起。SYN 0灭 “这是一封普通的、处于通信中的信”。只要连接一旦建立成功第三次握手及以后这盏灯就会永久熄灭再也不会亮起。2. ACK (Acknowledge) —— “收到确认”信号灯这盏灯是 TCP 可靠传输的灵魂用来给对方报平安。ACK 1亮 “我这封信里的确认号 ack 是有效的我收到了你之前发的数据。”TCP 规定除了客户端发出的第一封信第一次握手以外后面所有人发出的所有报文ACK 都必须是 1。只要连接成了大家说话都得带着“收到”的标志。ACK 0灭 “这封信里的确认号 ack 无效忽略它。”这种情况极其罕见只有在第一次握手时客户端单方面发起连接此时它还没收到服务端的任何回应所以 ack 没意义此时 ACK 0。3. FIN (Finish) —— “再见/结束”信号灯这盏灯专门用来拆除连接。它亮起代表一方要开始“撂电话”了。FIN 1亮 “我的应用数据已经全部发完了我要关闭我这边的发送通道了。”在四次挥手时客户端想断开会发一封 FIN1 的信服务端想断开也会发一封 FIN1 的信。它亮起就意味着“再见”。FIN 0灭 “我还在正常传输数据呢没打算断开。”在平时的正常通信、以及建立连接的三次握手期间这盏灯都是灭的。如果把 TCP 通信比作两个人在通过挂号信写一本连载小说那么 seq 和 ack 就是书页里的“字符计数器”。因为 TCP 是全双工双向通信所以每个人手里都有一个独立的计数器。4. seq序号当前发送的“起点位置”TCP 极其严谨它不按“第几条消息”来算账而是按“字节Byte”来算账。只要连接一建立TCP 就会为你要发送的每一个字节的数据在心里编一个号。seq 的含义就是告诉对方“这封信里装的数据是从我这边全局数据流的第几个字节开始的”。5. ack确认号期望下次收到的“起点位置”ack 则是用来向对方“催更”或“报平安”的。ack 的含义告诉对方“到第 X-1 个字节为止的数据我全都稳稳收到了你下一封信请从第 X 个字节开始发给我。”这里为什么是 x - 1?注意这里是数据传输时的ack传输过程中是有一个实际的len数据长度的 如果对方这次的起点 seq 101数据长度 len 100。 对方发过来的字节编号范围从 101 开始往后数 100 个最后一个字节的编号是 200。 公式计算 ackseq len $101 100 $ 201这就是 $X$。 代入大白话 “到第 $201 - 1 200$ 个字节为止的数据我全都收到了你下一封信请从第 201 个字节开始发。” 核心逻辑 因为 TCP 算的是“下一发数据的起始位置”。 ack 的数值$X$代表的是尚未收到的、排在最前面的那一个空位。既然它是第一个没收到的那它紧挨着的上一个位置$X - 1$自然就是最后一个已稳稳收到的。 三次握手时的 ack x 1,注意这里的1是默认长度不要和上面的x - 1联想到一起不一样的概念先了解下这个1具体三次握手过程我们看后面 核心特殊规则没有数据强行 1 在正常的通信中ack seq len数据长度。 但在三次握手和四次挥手时报文里是没有应用层数据的也就是说 len 0。 如果按照常规公式算ack seq 0 seq那对方下一次发过来的起点还是 seq这就原地打转了。 为了解决这个问题TCP 规定只要控制位里的 SYN同步或者 FIN结束标志灯亮起为 1这个报文就算没有任何数据也必须硬性、强行占掉 1 个字节的编号位置 因此三次握手时的计算公式变成了ack 对方的 seq 1。 所以ack 的数值永远是对方刚发过来的 seq 对方刚发过来的数据长度。二、上述知识了解后我们来看看三次握手1. 第一次握手客户端发起连接做什么客户端随机生成一个初始序号 seq x把控制位 SYN 设为 1向服务端发送首个报文。为什么没有 ack 因为这是第一封信客户端还没有收到过服务端的任何回应ACK 灯是灭的0此时的 ack 没有任何意义通常为 0。状态变化客户端由 CLOSED关闭状态进入 SYN_SENT同步已发送 状态。此时底层这个报文不能携带任何应用层数据如 HTTP 请求但它会消耗掉一个序号所以下一步对方回的 ack 会是 x 1。2. 第二次握手服务端响应并反向请求做什么服务端收到 SYN 报文后同意建立连接。它需要做两件事确认客户端的请求把 ACK 设为 1并将确认号设为 ack x 1。这里的 ack 计算公式对方的 seq 强行 1。大白话就是“客户端到第 x 个字节为止的‘建立连接暗号’我全都稳稳收到了你的诚意我懂了。接下来你再跟我说话第三回合请从第 x 1 个字节开始发给我”自己也发起连接请求随机生成自己的初始序号 seq y把 SYN 设为 1。这两个动作合并成一个 SYNACK 报文发回。状态变化服务端由 LISTEN监听进入 SYN_RCVD同步已接收 状态。3. 第三次握手客户端最后确认做什么客户端收到服务端的 SYNACK 报文。对服务端的连接请求进行确认把 ACK 设为 1确认号设为 ack y 1自己的序号变为 seq x 1。发送给服务端。这里的 ack 根据公式对方的 seq 强行 1。大白话“服务端你发过来的连接请求我也收到了到第 y 个字节为止的暗号我全拿到了你放心吧。咱们现在连接建好了你接下来如果要给我发真正的网页数据请从第 y 1 个字节开始发给我。”状态变化客户端发送后立刻进入 ESTABLISHED已建立连接 状态。服务端收到该报文后也进入 ESTABLISHED 状态。此时底层第三次握手的报文可以携带具体的应用层数据了。 三次握手时的 ack其本质就是“给对方的连接请求SYN打一个回执” 服务端回的 ack 101是对客户端 SYN (seq100) 的签收证明。 客户端回的 ack 501是对服务端 SYN (seq500) 的签收证明。 正因为双方都拿到了对方明确写着 1 的 ack 回执彼此才敢百分之百地确信“对方不仅听到了我的声音而且我们的序列号已经完美对齐了接下来可以放心大胆地传文件了” 深度思考为什么“两次”不行“四次”不多余吗 为什么不能是两次为了防止已失效的连接请求突然传到服务端引起错误 假设场景客户端发了第一个 SYN但在网络堵车了。客户端以为丢包了于是重发了第二个 SYN。这次网络很顺两次握手成功传完数据连接关闭了。 问题来了这时候那条堵车的第一个 SYN 终于开到了服务端。如果只要两次握手服务端就会认为客户端又发起了一个新连接于是进入 ESTABLISHED 状态在原地苦苦等待。但客户端实际上根本没想发数据服务端的资源就被白白浪费了。 有了第三次握手服务端收到迟到的 SYN 即使回了 SYNACK客户端发现自己没发过这个请求就不会理它服务端迟迟收不到第三次 ACK就会知道这个连接是无效的。 三、四次挥手TCP 的连接是双向全双工的断开时必须两端分别独立关闭。1. 第一次挥手客户端主动关闭发送做什么客户端不想发数据了发送一个 FIN 1 的报文随机生成序号 seq u。状态变化客户端进入 FIN_WAIT_1终止等待1 状态。注意此时客户端进入“半关闭”状态它不能再发送应用数据但如果服务端发来数据客户端依然可以接收。2. 第二次挥手服务端确认进入半关闭做什么服务端收到 FIN 报文发送一个 ACK 1 进行确认确认号 ack u 1带上自己的当前序号 seq v。状态变化服务端进入 CLOSE_WAIT关闭等待 状态。这时候应用层会被通知对方要断开了。客户端收到这个 ACK 后进入 FIN_WAIT_2终止等待2 状态。此时底层此时 TCP 连接处于半关闭Half-Close。客户端到服务端的这条路断了但服务端如果还有数据没发完可以继续往客户端发。3. 第三次挥手服务端数据发完申请关闭做什么服务端把最后缓存的数据全部发送完毕后也想关闭连接了。于是向客户端发送 FIN 1, ACK 1 的报文由于中间可能发了些数据它的当前序号变成了 seq w确认号依然保持 ack u 1。状态变化服务端进入 LAST_ACK最后确认 状态。4. 第四次挥手客户端最终确认做什么客户端收到服务端的 FIN 报文必须最后回复一个 ACK 1 确认ack w 1seq u 1。状态变化服务端收到该 ACK 后直接进入 CLOSED 状态服务端断开完成。客户端发送后进入 TIME_WAIT时间等待 状态。在这个状态死等 2MSLMaximum Segment Lifetime报文最大生存时间之后如果没有异常才最终进入 CLOSED 状态。四、思考和问题1. 为什么第四次挥手后客户端必须在 TIME_WAIT 状态等 2MSL为了保证最后一次 ACK 能够安全送达服务端网络是不可靠的。假设客户端发出的第四次 ACK 在路上丢包了服务端在 LAST_ACK 状态下迟迟收不到确认就会认为自己的第三次 FIN 丢了于是重发 FIN。如果客户端发完 ACK 就直接 CLOSED 了当服务端的重发 FIN 到达时客户端已经找不到对应的连接了就会回复一个 RST复位重置报文这会导致服务端报错通常是 Connection reset by peer无法优雅地正常关闭。客户端等 2MSL一去一回的最长时间就能保证如果服务端没收到能在该时间内重发客户端也能在这等它并重新补发 ACK。防止“已失效的连接请求报文”出现在新连接中等待 2MSL 的时间足以让本次连接在网络中产生的所有残留报文比如在某个路由器里堵车延迟的报文全部死掉并消失。这样当下次你再用相同的 IP 和端口建立新连接时就绝不会收到上一次连接遗留下来的脏数据2. 什么是 已失效的连接请求报文假设你在家里用电脑连接服务器整个故事分为前世和今生前世上一次连接客户端发送了一个连接请求SYN 报文假设它的序列号 seq 100。结果这个报文走到半路网络大堵车它被卡在某个慢吞吞的路由器缓存里了。客户端等了半天没回应以为丢包了于是重新发了一个 SYNseq 200。这次很顺利三次握手成功数据传完了双方四次挥手正常断开。重点来了 那个卡在半路的第一个 SYNseq 100依然活着它在网络里继续晃悠。这个报文就叫做“已失效的连接请求报文”。今生紧接着发起的新连接如果挥手后不等待会发生什么恐怖的事上一次连接断开后你马上又用相同的 IP 和相同的端口号向同一个服务器发起了新连接。就在新连接刚刚建立好双方准备传数据的时候那个在网络里堵车很久的、前世遗留下来的 SYNseq 100突然满血复活终于开到了服务器如果四次挥手时客户端没有 TIME_WAIT 的限制而是发完最后一个 ACK 立刻彻底关闭那么服务器看到这个延迟大半天的 SYN误以为是客户端又发起了一次全新的请求。服务器就会一本正经地回复 SYN ACK。此时客户端会莫名其妙地收到一个来自前世的确认报文导致整个 TCP 的状态机彻底陷入混乱甚至可能把前世没发完的旧数据混进今生的新数据里导致传输的文件损坏3. 四次挥手时的 TIME_WAIT 是如何解决这个问题的为了消灭这种“跨时空污染”TCP 规定在四次挥手的最后一步客户端发送完最后一个 ACK 后连接并没有真正关闭而是把端口锁死进入 TIME_WAIT 状态死等 2MSL 时间。MSLMaximum Segment Lifetime是一个报文在网络里能活下来的最大时间。如果超过这个时间网络里的路由器就会无情地把它丢弃。在现代 Linux 操作系统中默认的 MSL 通常被硬编码设置为 30 秒所以 2MSL 就是 60 秒。在老旧的系统或标准的 TCP 规范RFC 793中官方建议的 MSL 甚至是 2 分钟2MSL 就是 4 分钟。为什么要等 2 倍2MSL 因为 1 个 MSL 保证客户端发出的最后一个 ACK 能到达对方万一这个 ACK 丢了服务端重发的 FIN 传回来又需要 1 个 MSL。这一来一回刚好是 2MSL。4. 2MSL 真正等的是谁TIME_WAIT 的 2MSL 时间是从第四次挥手客户端发送最后一个 ACK的那一刻才开始倒计时的。注意这里2msl只是从第四次挥手那一刻开始倒计时并不只是为了熬死第四次发出去后失效的报文而是为了熬死所有属于这次连接的、活着的报文它等的是在挥手前后这一小段时间内网络里可能刚刚产生的、还在赶路的、属于本次连接的所有残余报文。也就是说TCP 强制全员在原地罚站 2MSL就是为了保证此时此刻网络中所有属于这次连接的、活着的报文全部超过 1个 MSL 的寿命而死绝。5. 从第四次挥手后有多个丢失的报文 2MSL 够用吗重传机制和最大重传次数限制绝对够用。不管丢了多少个报文2MSL 都是理论和实际上的“终极安全边界”。这里的核心秘密在于2MSL 的倒计时并不是一成不变的它是可以被“无限续期刷新”的。场景推演如果第四次挥手的 ACK 丢了假设客户端发出第四次挥手的 ACK并且网络里还有其他残留报文在乱飞客户端 发出最后一个 ACK开始 2MSL 倒计时假设一共要等 2 分钟。网络中 这个 ACK 走到半路丢了。服务端 因为没收到 ACK触发了超时重传机制。服务端会认为“我刚才发的第三次挥手 FIN 是不是丢了” 于是服务端重新发送了一个 FIN。客户端 还在 2MSL 的“原地罚站”状态中。突然它又收到了服务端重传的 FIN。【关键机制触发】 客户端只要在 TIME_WAIT 期间收到任何来自对方的旧报文比如重传的 FIN它就会重新发送一次 ACK并且把 2MSL 的定时器直接清零重新开始倒计时 2 分钟为什么说不管丢多少个报文都够用通过上面的重传机制你会发现 2MSL 的真正威力在于只要网络里还有“活着的旧报文”折腾 服务端就会因为收不到确认而不断重传 FIN。客户端每收到一次 FIN就会续命刷新一次 2MSL。直到什么时候才停止 直到某一时刻客户端发出的 ACK 终于成功送达服务端服务端彻底关闭CLOSED不再重传了或者服务端重传达到最大次数强制断开。最后的平静 当网络里最后一个重传的 FIN 报文传到客户端或者在路上死掉之后网络里就再也没有任何新的报文产生了。2MSL 开始最后的清空 客户端以这“最后一个报文出现的时间”为起点孤独地等待完整的 2MSL。1 个 MSL 保证客户端最后补发的那个 ACK 要么到了要么死在路上了。第 2 个 MSL 保证服务端如果没收到、再次重传的 FIN如果还有的话在通往客户端的路上彻底死掉。当这最后的 2MSL 顺利走完意味着全网所有可能在赶路的、重传的、迷路的、错乱的报文已经全部超过寿命上限无一幸免全部死绝了。总结2MSL 够用不是因为时间长而是因为 2MSL 永远是以“网络中最后一点风吹草动”为起点开始计算的。它是一座永远能帮网络“清空历史、重头再来”的终极安全防御塔。6. 为什么是2MSL 不是1MSL?等 1 个 MSL 只能保证“单向”的报文死绝而等 2 个 MSL才能保证“一去一回”双向的报文在全网彻底死绝。因为第四次挥手时客户端面临的不是一个平静的网络它刚刚发出了最后一个 ACK而服务端可能还在痴痴地等。为了搞懂为什么必须是 2 倍我们来推演一下如果只等 1 个 MSL网络会发生怎样惨烈的“连环撞车事件”致命推演为什么 1 个 MSL 绝对不够假设我们把时间卡在第 0 秒客户端向服务端发送了第四次挥手的最后一个 ACK。此时这个 ACK 在网络中最多能活 1 个 MSL。倒霉鬼场景第 0 秒 客户端发出最后的 ACK。第 0.9 个 MSL 的时候 这个 ACK 运不好走到半路丢包死掉了。服务端开始发飙 服务端在左等右等了将近 1 个 MSL 之后依然没有收到 ACK。由于服务端的超时重传机制它判定自己刚才发的第三次挥手 FIN 丢了。于是服务端在第 1 个 MSL 的瞬间重新打包并发送了一个新的 FIN 报文。客户端如果只等 1 个 MSL 就在服务端发出重传 FIN 的同一秒客户端的“1个MSL定时器”到期了。客户端开心地想“太好了1个MSL到了我自由了” 于是客户端瞬间关闭了端口CLOSED。灾难发生了服务端在第 1 个 MSL 时发出的那个重传的 FIN才刚刚出发啊这个全新的 FIN 报文在网络里又可以活 1 个 MSL。它晃晃悠悠地往客户端飞去。此时客户端已经关闭了或者更糟——客户端在第 1.001 个 MSL 的时候立刻用相同的 IP 和端口启动了一个新连接结果在第 1.5 个 MSL 的时候那个服务端重传的旧 FIN 突然拍在了客户端新连接的脸上。新连接瞬间风中凌乱直接崩溃。2MSL 的精妙之处闭环的时间对称性为了堵住上面这个由于“超时重传”导致的致命时间差计算机科学家设计了 2个 MSL。这 2个 MSL 的分工极其明确刚好对应一去一回的极限时间第 1 个 MSL“去”的极限用来等待客户端发出的最后一个 ACK 走到服务端。如果这个 ACK 顺利走完 1 个 MSL 还没到说明它在路上死了。第 2 个 MSL“回”的极限如果 ACK 死了服务端会在最极限的时刻大约第 1 个 MSL 结束时触发重传把旧的 FIN 再次拍出来。这个新出来的 FIN 飞回客户端又最多需要 1 个 MSL 的时间。总结客户端在第四次挥手后老老实实原地罚站 2MSL本质上是在用时间做一堵双向防火墙如果网络好ACK 在第 1 个 MSL 内到了服务端安静关闭。剩下的 1 个 MSL 时间里网络一片寂静客户端安全退出。如果网络坏ACK 丢了服务端重传的 FIN 必然会在 2MSL 期间传到客户端。客户端一旦收到就会立刻重发 ACK 并刷新重置2MSL 定时器重新再等 2 分钟直到网络彻底干净为止。所以1 个 MSL 只能保证自己发的信息死透2 个 MSL 才能保证对方因为没收到而产生的“报复性重传”也死透。 TCP 用这个天才的对称性设计换来了互联网几十年的稳定。7. 为什么这2MSL等待不在服务端 而在客户端?这里有两个最核心的底层设计考量“谁主动谁负责” 的原则以及对 服务器资源的保护。核心铁律谁主动关闭2MSL 就在谁身上其实TCP 协议并没有规定 TIME_WAIT 必须在“客户端”。它的准确规定是谁先发起主动关闭Active Close2MSL 的等待就由谁来承担。在绝大多数网络场景下比如你用浏览器访问网站都是客户端传完数据、看完网页后主动划掉页面或者断开连接。因为是客户端主动发起的断开所以客户端承担了这 2MSL 的等待。如果网站服务器因为某些原因比如超时、或者垃圾清理主动把你的连接断开了那这个 2MSL 的 TIME_WAIT 状态就会留给服务器。为什么要把这个负担“尽量”压在客户端身上原因一服务器的端口资源极其宝贵怕端口耗尽客户端的视角 客户端作为一台个人电脑同一时间可能也就建立几十个、几百个网络连接。让它拿出一个端口原地罚站 2 分钟无伤大雅用户根本感知不到。服务端的视角 服务器比如一台高并发的 Web 服务器每秒钟可能要处理几万个用户的连接。如果每个用户离开后服务器都要为这个连接默默死等 2 分钟那么短时间内服务器上就会堆积几十万个处于 TIME_WAIT 状态的死连接。后果 操作系统的端口号和文件描述符是有限的通常一个 IP 最多 65535 个端口。如果全部被 TIME_WAIT 占满了服务器就会陷入“端口耗尽”的窘境再也无法接收任何新用户的请求了。原因二符合“保全大局”的设计哲学在网络架构中有一个核心原则叫“把负担和状态尽量推向网络的边缘客户端保证网络核心服务端的高效和轻量。”客户端崩了只影响这一个小明但如果服务器因为端口被死等的连接占满而崩了全天下几万个小明、小红都无法访问网站了。所以这个“负责收尾清空幽灵报文”的安全重任自然要由客户端来扛。如果服务端不得不“主动关闭”怎么办在实际开发中有时候服务器必须主动断开不听话的客户端比如客户端恶意占用连接不发数据。这时服务器就会不可避免地进入 TIME_WAIT 状态。为了防止上面说的“服务器端口耗尽”导致瘫痪现代网络编程和操作系统提供了几种高级外挂SO_REUSEADDR端口复用这是后端开发中最常用的套接字选项。它告诉操作系统“就算这个端口现在处于 TIME_WAIT 状态只要新来的连接序列号对得上允许新连接直接抢用这个端口”直接打破 2MSL 的死等限制。发出 RST 报文直接闪退服务器如果想彻底甩开 2MSL 的纠缠可以直接向客户端发送一个 RST复位报文而不是走正常的四次挥手。这相当于粗暴地直接挂断电话不触发四次挥手也就完全不会产生 TIME_WAIT 状态。总结2MSL 留给客户端是因为客户端往往是主动说再见的那个人。TCP 协议让“主动说再见的人负责留下来打扫战场”既能保证网络数据的绝对安全又能完美保护服务器不被海量的死连接活活拖垮。