尧图精选

从TCP三次握手到四次挥手:Python网络编程面试必懂原理

🕒 发布时间:2026/10/1 3:41:34 📁 来源:尧图网络
“简述TCP三次握手以及四次挥手的流程为什么需要三次握手以及四次挥手。”——这几乎是每份Python后端面试题库里都会出现的一道经典题。我既是面试官也当过求职者见过太多人在这一步翻车流程背得滚瓜烂熟被追问一句“为什么是三次而不是两次”就卡壳。这说明大多数人只是背了答案没有真正理解TCP作为一个“带状态的可靠传输协议”到底在解决什么问题。今天这篇文章就把这题拆开讲透。先按报文级别拆解三次握手和四次挥手再解释为什么偏偏是三次、四次而不是两次、三次最后用Python的socket代码把整个过程实际跑一遍顺带聊聊CLOSE_WAIT和TIME_WAIT这两个面试追问率极高的坑。适合准备Python后端、爬虫、网络相关岗位的求职者也适合写了不少socket代码却从没系统梳理过协议细节的工程师。1. 为什么这道题躺在Python面试题里这么多年1.1 一道看似基础实则分层很开的问题TCP三次握手和四次挥手这道题表面上是问“流程”但实际上是一个分层很开的考察点。初级候选人能讲出“SYN、SYN-ACK、ACK”和“FIN、ACK、FIN、ACK”就算过关中高级候选人会被追问到序列号怎么算、状态怎么迁移、为什么不能少一次握手、TIME_WAIT有什么用。同样的题目面试官可以根据候选人的回答深度迅速判断出这个人对网络的理解停留在背诵层面还是原理层面。这也是为什么Python岗位的面试题里会反复出现它。Python开发者日常打交道最多的网络场景——用requests写爬虫、用Flask/Django写Web服务、用socket写长连接、用websocket做消息推送——底层几乎全部建立在TCP之上。连接能不能建立、连接为什么断开、服务端为什么出现大量CLOSE_WAIT这些问题排查到最后全都要回到握手和挥手这一层来理解。所以这道题不是网络工程岗位的专属而是所有写网络程序的工程师的必修课。Python本身封装了socket API你调用一次connect()背后就是一次完整的三次握手你调用close()背后就是一次四次挥手。不理解这个过程出了问题就只能瞎猜连排查方向都找不到。1.2 面试官通过这道题在看什么作为面试官我问这道题时内心其实在看三件事。第一你有没有真正理解“可靠传输”这四个字。TCP不是简单地“把数据发出去”它要保证数据不丢、不重、不乱序这一切的基础就是连接建立时的协商机制。三次握手不只是打招呼而是在不可靠的网络环境里让通信双方确认“我能发、你能收、你能发、我能收”并且同步彼此的初始序列号。第二你能不能把状态机讲清楚。TCP每一个状态都不是凭空设计的连接建立和释放过程中的每个状态变化都对应网络中的某个事件。比如服务端收到SYN会进入SYN_RCVD主动关闭方发出FIN会进入FIN_WAIT_1被动关闭方收到FIN后如果应用层没有及时close就会一直停在CLOSE_WAIT。能把这些状态串起来讲说明你对TCP的理解是成体系的。第三你会不会把理论知识用到实际排错里。这是区分“背题选手”和“实战选手”的关键分水岭。面试官大概率会追加一句“你线上服务大量CLOSE_WAIT怎么排查”或者“为什么爬虫跑久了客户端有大量TIME_WAIT”如果你能直接答出“CLOSE_WAIT是被动关闭方应用没调closeTIME_WAIT是主动关闭方频繁建短连接”这题就稳了。2. 三次握手完整流程从报文到状态机2.1 三次握手到底做了哪三件事TCP三次握手发生在客户端主动发起连接时完整过程是客户端发送SYN报文服务端回复SYNACK报文客户端再回复ACK报文。三次握手完成后双方都进入ESTABLISHED状态连接建立成功。我用一个生活化的类比来帮助理解。你想约朋友去一家餐厅你发消息说“我周六晚上七点想到这家餐厅”这是第一次握手SYN。朋友回复“我周六晚上七点可以你也确认一下你收到我的消息了”这是第二次握手SYNACK。你收到后回复“好我确认收到”这是第三次握手ACK。只有经过这三次确认双方才都确定对方在、自己的消息对方也收到了、约定的时间和地点没问题。放到TCP里三次握手解决的是三个问题第一次握手客户端发SYNseqx服务端确认“客户端的发送能力正常且我愿意接受这次连接”。第二次握手服务端发SYNACKseqyackx1客户端确认“服务端的接收和发送能力都正常”同时得知自己的SYN被对方正确收到。第三次握手客户端发ACKacky1服务端确认“客户端的接收能力正常”同时得知自己的SYNACK被对方正确收到。注意第二次握手这个动作非常关键它把SYN和ACK两个标志位放在同一个报文中是因为服务端在回复客户端的同时自己也要发起一个方向上的连接协商。本质上三次握手可以理解为“一次双方各自发起、但是互相交错确认”的过程。2.2 每一步的序列号是怎么算的握手过程除了标志位最重要的就是序列号。很多人背流程只记住了SYN、ACK忽略了seq和ack但恰恰是这两个数字决定了TCP的可靠传输基础。先明确几个关键点seq序列号表示本报文段第一个字节的序号新连接建立时由双方各自随机生成一个初始序列号ISN。ack确认号表示“我期望收到对方的下一个字节的序列号”也就是对方最后一个收到的seq加一。SYN和FIN报文本身虽然不携带应用数据但要消耗一个序列号所以计算ack时要加一。三次握手的序列号变化如下步骤方向标志位seqack第一次客户端 - 服务端SYN1x客户端ISN无效不携带第二次服务端 - 客户端SYN1, ACK1y服务端ISNx1第三次客户端 - 服务端ACK1x1y1这里有个面试常问的点为什么第二次握手的ack是x1而不是x因为SYN报文虽然没有应用数据但它本身占据一个序列号空间。客户端发送的SYN其序列号为x服务端确认收到这个SYN期望客户端下一个报文从x1开始所以ackx1。这个机制的意义在于双方在握手阶段就完成了ISN的交换。之后的每个数据包都会带着基于这个初始序列号递增的seq接收方依靠ack来告诉对方“我收到了哪些数据我还缺哪些数据”这样丢包、重传、乱序才有了判断依据。2.3 握手过程中的系统资源与队列研究握手流程时还有一个被忽视但面试常考的话题连接建立过程中服务端经历了两个队列。客户端发送SYN后服务端会创建一个半连接放入半连接队列syn queue此时服务端状态为SYN_RCVD。只有收到客户端的第三次ACK后服务端才会把连接从半连接队列移入全连接队列accept queue状态变为ESTABLISHED等待应用调用accept()取出。两个队列的区别是实战中非常关键的知识点。如果服务端应用处理连接的速度跟不上新连接到达的速度全连接队列会满新的连接就无法被accept客户端表现为connect成功但请求无响应如果恶意客户端只发SYN不回复ACK半连接队列会被占满合法的连接请求也会被丢弃这就是经典的SYN Flood攻击原理。在Python里socket.listen(b)里的b参数并不是限制最大连接数而是限制全连接队列中未accept的已完成连接数量。很多人误以为listen(5)是“最多允许5个客户端连接”实际上它表示内核全连接队列最多积压5个已完成三次握手、等待accept的连接。这个误解在面试里很常见现在记住listen的 backlog 控制的是accept队列长度不是最大连接数。3. 为什么必须是三次最容易被忽略的底层原因3.1 收发能力的相互确认需要三次面试官最爱问的一个问题就是为什么不能是两次握手答案要从“全双工通信的确认需求”说起。TCP是全双工协议数据在两个方向上独立传输。一条连接要想正常工作双方必须都确认我能发数据你能收数据你能发数据我也能收数据。也就是说需要四个维度的确认客户端发送能力、服务端接收能力、服务端发送能力、客户端接收能力。用握手过程来匹配这四个维度第一次握手后服务端收到SYN服务端能确认客户端能发SYN发出去了、服务端自己能收收到SYN了。但此时服务端不知道客户端能不能收。第二次握手后客户端收到SYNACK客户端能确认服务端能发SYNACK收到了、客户端自己能收收到SYNACK了、服务端能收它回复了我的SYN说明我的SYN到达了。此时客户端已经确认了自己的收发能力都OK但它不确定服务端的接收确认是否可靠传导。第三次握手后服务端收到ACK服务端才能确认客户端能收ACK收到了。此时服务端才能确定“我发的SYNACK确实到了客户端那里”。如果只有两次握手第二次握手完成后客户端已经确信双方收发正常但服务端不确定客户端是否收到了自己的SYNACK。也就是说服务端无法确认客户端接收能力是否正常更无法确认自己发送的初始序列号是否被客户端接受。3.2 初始序列号的同步必须双方确认第二次握手除了确认收发能力还承载一个重要任务服务端把自己的初始序列号y发给客户端。但服务端要确认客户端真的收到了这个y并且客户端也在自己的确认号里接受了它。这就要靠第三次握手来完成。客户端在第三次报文中发送acky1这既是对服务端SYN的确认也意味着“我已经收到了你的初始序列号后续我会按照这个序列号基准来接收你的数据”。如果只有两次握手服务端发出自己的ISN后就认为连接建立但客户端可能根本没有收到这个ISN后续双方对数据的编号理解就会错位可靠传输根本无从谈起。所以可以说第三次握手承载了“ISN同步确认”这个不可省略的使命。3.3 防止历史重复报文造成资源浪费这是三次握手最经典、也最能体现设计巧妙性的原因面试答出来基本就是加分项。场景是这样的客户端发送了一个SYN报文请求建立连接但这个报文在网络中因为拥堵被延迟了很久。客户端等不到回复超时重传了一个新的SYN。这两个SYN的初始序列号不同假设旧的是seq100新的是seq200。如果只有两次握手当旧SYNseq100终于到达服务端时服务端会认为这是一个新的连接请求立刻进入ESTABLISHED状态并分配资源。但实际上客户端早就放弃了这个连接它期望建立的是seq200的新连接。结果就是服务端建立了一个客户端根本不需要的“闲置连接”白白消耗资源。三次握手可以完美解决这个问题。服务端收到旧SYN后回复SYNACKseq服务端ISNack101。客户端收到后发现咦这个ack101不是我所期望的201这不是我为当前连接发出的SYN。于是客户端发送一个RST报文中止这个半连接。服务端收到RST后清理掉该连接不会让它进入ESTABLISHED。随后客户端真正的新SYN到达双方正常完成三次握手。这个设计解决的本质问题是在不可靠的网络中旧报文可能迟到连接建立机制必须有能力识别并拒绝这种“已经过期的连接请求”避免资源浪费。两次握手做不到这一点三次握手通过“双方确认序列号一致”实现了历史报文的甄别。4. 四次挥手全双工关闭的真正含义4.1 四次挥手的完整过程与状态迁移连接关闭比建立要复杂因为TCP是全双工的每个方向都必须独立关闭。四次挥手的过程以客户端主动关闭为例第一次挥手客户端发送FIN报文sequ表示“我的数据发完了我这边不再发送数据但如果你还有数据我仍然可以继续接收”。客户端进入FIN_WAIT_1状态。第二次挥手服务端回复ACKacku1表示“我收到了你的关闭请求但我这边可能还有数据要发”。服务端进入CLOSE_WAIT状态客户端收到后进入FIN_WAIT_2状态。第三次挥手服务端数据处理完毕发送FIN报文seqv表示“我这边数据也发完了现在可以关闭连接了”。服务端进入LAST_ACK状态。第四次挥手客户端回复ACKackv1经过TIME_WAIT后连接彻底关闭。客户端进入TIME_WAIT状态服务端收到ACK后进入CLOSED状态。这个过程的重点在于第二次和第三次挥手之间的间隔并不是固定的。服务端收到FIN后可能还有数据要发送它会在数据全部发送完毕后再发出自己的FIN。这个间隔可能很短也可能很长取决于应用层什么时候处理完数据并调用close。4.2 为什么挥手需要四次而不能合并面试官第二个高频追问是为什么关闭连接需要四次不能像握手那样把第二次和第三次合并成一次变成三次挥手原因在于握手是双方空闲状态下同步发起协商挥手则存在“一方已经结束另一方还在工作”的不对称状态。主动关闭方发送FIN时它的发送方向已经关闭但数据可能仍在传输通道里向被动方流动被动方收到FIN时只是知道了“对方不发数据了”它自己的发送方向还处于可用状态可能还有未发送的数据。第二次挥手只是一个确认告诉对方“我收到你的关闭请求了”。此时被动方不能立刻关闭自己的发送方向因为数据还没发完。所以FIN必须推迟到被动方的数据全部发送完毕后再发出。换句话说ACK和FIN分离的关键原因是被动关闭方可能还有数据要发送关闭发送方向的时机不由自己收到FIN的那一刻决定而由应用层数据处理完毕的那一刻决定。这个过程是独立且可能滞后的因此ACK和FIN无法合并成一次发送挥手自然需要四次。4.3 TIME_WAIT与2MSL等待四次挥手完成后主动关闭方不会立刻进入CLOSED而是停留在TIME_WAIT状态持续最长4分钟2MSL即两倍最大报文段生存时间。很多Python面试题会专门问这个状态理解它需要抓住两个原因。第一个原因保证最后一次ACK能可靠到达。第四次挥手发送的ACK可能丢失届时候被动方会在超时后重发FIN主动方需要重新回复ACK。如果主动方在发送ACK后立刻关闭连接这个ACK一旦丢失被动方就会一直停留在LAST_ACK状态连接永远无法正常关闭。TIME_WAIT让主动方在2MSL内保持可响应状态如果收到被动方重发的FIN可以再次回复ACK。第二个原因确保本连接产生的旧报文在网络中消逝。一个报文从发出到失效最长需要MSL时间。而最坏情况是某个报文从A到B需要MSL对端回复又需要MSL所以往返需要2MSL。主动方等待2MSL保证了本连接关闭前发出的所有报文都已经在网络中消失不会干扰后续可能复用相同端口的新连接。面试中还有一个关于TIME_WAIT的现实问题如果服务端是主动关闭方它会积累大量TIME_WAIT连接占据端口和内核资源。Python开发中常见的一个场景是使用短连接频繁连服务端每完成一次请求就关闭连接服务端作为主动关闭方会残留大量TIME_WAIT。这是为什么很多高并发服务要开启keep-alive长连接的原因之一——减少不必要的连接创建和销毁。5. 面试追问变体三次挥手什么时候成立5.1 被动关闭方恰好同时关闭的特殊情况面试官往往会在你答完四次挥手后追加一句难道挥手就一定是四次吗能不能是三次答案是可以但需要满足一个特定条件——被动关闭方在收到FIN报文时恰好自己的数据也已全部发送完毕应用层立即发起close。这种情况下被动方可以把第二次挥手的ACK和第三次挥手的FIN放在同一个报文中发送原本的四次挥手就变成了三次客户端发FIN服务端回FINACK客户端回ACK。这个场景在实际中比较少见因为通常被动方不会恰好在收到对方FIN的瞬间就处理完所有数据。但只要发生抓包时看到的挥手报文数量就是三个而不是四个。面试时能主动说出这个变体说明你对协议的理解不是死记硬背的。还有一种更少见的场景是双方同时关闭simultaneous close客户端和服务端同时发送FIN双方都在收到对方FIN的同时也发出了自己的FIN。这种情况下双方都会经历从FIN_WAIT_1直接进入CLOSING状态再进入TIME_WAIT挥手报文仍然是四个方向交错但状态迁移路径和标准的顺序挥手不同。掌握这个场景可以在面试里作为加分项补充不过优先级低于前面所有内容。5.2 RST异常断开不走挥手流程除了正常的四次挥手TCP还有一个异常断开方式——发送RST报文。RST表示“连接异常终止”收到RST的一方会立即丢弃该连接上的所有数据直接进入CLOSED状态不进入TIME_WAIT也不走FIN/ACK的协商流程。什么时候会触发RST典型场景有三种一是连接双方中有一方崩溃重启内核发现socket表里没有对应连接收到旧连接的数据包时回复RST二是对方发送的数据包序列号不在窗口范围内三是应用层调用SO_LINGER设置了特殊选项后主动发送RST关闭。对Python开发者来说最常见的RST场景是对方程序崩溃退出你继续向这个连接写数据然后收到ConnectionResetError。很多爬虫新手在抓取网页时突然遇到“Connection reset by peer”就是这个原因。理解了RST机制排查这类异常时就能直接定位到“对端已经不在正常关闭流程里了”从而检查对方服务是否健康。5.3 半关闭状态的实际应用四次挥手过程中其实存在一个半关闭阶段客户端发送FIN后它的发送方向已经关闭但仍然可以接收数据。如果你用Python的socket开发可以调用shutdown(socket.SHUT_WR)来实现半关闭——告诉对端“我不再发送数据了但我会继续读你发来的数据”而不是直接关闭整个socket。这个机制在面试里也有考点如果主动关闭方只想关闭发送方向但还希望继续接收对端的剩余数据就应该用shutdown而不是close。close会同时关闭读写两个方向而shutdown可以只关写方向让对端有机会把最后的数据发完然后对端再关闭连接。在HTTP协议中很多场景都用到了半关闭特性客户端发送完请求后关闭写方向服务器读完请求后知道已经不会再有更多请求数据于是处理完响应后关闭自己的发送方向。如果你用Python写过简单的HTTP服务器会发现在读取完请求头、请求体之后需要处理EOF或半关闭信号才能正确判断“客户端请求已经发完”。6. 用Python实际验证一次代码、抓包与常见故障6.1 从socket API看握手到挥手的完整映射理论讲完用Python代码把全过程跑一遍。先写一个最简单的TCP服务端和客户端。服务端import socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(5) print(server listening on 8888) conn, addr server.accept() print(faccept connection from {addr}) data conn.recv(1024) print(freceived: {data.decode()}) conn.send(bhello from server) conn.close() server.close()客户端import socket client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((127.0.0.1, 8888)) print(connected) client.send(bhello from client) data client.recv(1024) print(freceived: {data.decode()}) client.close()这段代码背后的协议行为是这样的。客户端执行connect()时操作系统会发送第一个SYN报文connect()会阻塞直到收到服务端的SYNACK并发出ACK然后返回。服务端的accept()返回一个已建立连接的socket对象说明全连接队列中已经有了一个完成三次握手的连接。客户端执行close()时操作系统发送FIN服务端recv()返回b表示对端关闭了写方向。如果服务端也调用close()它发出了自己的FIN客户端完成第四次ACK连接关闭。代码里服务端先发送响应数据再close正好能演示四次挥手中“服务端收到FIN后可能还有数据要发送”的场景。6.2 运行过程中的状态观察用命令行工具可以直接观察到握手和挥手的状态变化。在Linux或macOS上先启动服务端再启动客户端然后执行netstat -ant | grep 8888在客户端connect成功后观察可以看到连接处于ESTABLISHED状态。此时立刻在服务端或客户端执行CtrlC关闭程序快速执行同样的netstat命令可以看到状态变化客户端主动关闭后短暂处于FIN_WAIT_2或TIME_WAIT状态。服务端收到FIN后如果应用没有及时close会处于CLOSE_WAIT状态。如果系统里有tcpdump还可以抓包验证每次握手和挥手的标志位sudo tcpdump -i lo0 port 8888 -n -X抓包输出里会清晰看到SSYN、FFIN、.ACK组合以及seq和ack的具体数值。建议自己把前面章节讲的序列号计算规律和抓包结果一一对应着核对一遍记忆会深刻很多。6.3 Python服务端大量CLOSE_WAIT的实战排查我在实际运维Python后端服务时最常见的故障之一就是服务端大量连接停留在CLOSE_WAIT状态。CLOSE_WAIT意味着客户端已经发送了FIN服务端的操作系统也回复了ACK但服务端应用层始终没有关闭这个socket。原因通常是代码里漏了close调用或者处理逻辑异常导致finally分支没有执行。比如用户开发了这样一个请求处理器def handle(conn): data conn.recv(1024) # 这里抛出了异常导致后续的conn.close()没有执行 result do_something(data) conn.send(result) conn.close()如果do_something抛异常后面的conn.close()永远执行不到这个连接就会一直停留在CLOSE_WAIT直到进程崩溃或被系统回收。正确写法是使用try-finally或上下文管理器确保socket一定被关闭def handle(conn): try: data conn.recv(1024) result do_something(data) conn.send(result) finally: conn.close()排查命令可以这样用找出CLOSE_WAIT连接的数量和对应进程netstat -ant | grep CLOSE_WAIT | wc -l ss -ant | grep CLOSE_WAIT这个错误在Python面试的编程题里也经常被埋进去面试官会故意让你看一段明明调用了recv但没关闭连接的代码问你这样写线上会出现什么问题。能答出“大量CLOSE_WAIT导致文件描述符耗尽服务端无法接受新连接”这题就过关了。6.4 爬虫客户端大量TIME_WAIT的应对思路与CLOSE_WAIT相对TIME_WAIT通常出现在主动关闭方。Python爬虫爱好者经常遇到这个问题用requests快速抓取大量网页默认每次请求都建立新连接请求完毕立刻关闭客户端作为主动关闭方就会产生大量TIME_WAIT。TIME_WAIT本身不是错误它是协议为了保证可靠关闭而必须经历的状态。但大量TIME_WAIT会让客户端可用的本地端口被占用因为TIME_WAIT状态下端口不能立刻复用最终可能导致“Cannot assign requested address”错误。应对思路有三个一是使用连接池让多个请求复用同一个TCP连接减少连接创建和关闭次数。requests的Session对象配合urllib3连接池就是为此设计的。二是启用SO_REUSEADDR允许端口在TIME_WAIT状态下被重新绑定但要注意这并不会绕过TIME_WAIT本身只是让bind更方便。三是服务端和客户端都尽量保持长连接避免频繁建连和断连。实测下来对爬虫场景最有效的还是连接复用既能减少TIME_WAIT又能降低握手带来的额外延迟。所以面试中被问“如何优化爬虫性能”除了并发和限速别忘了从TCP连接生命周期这个角度回答会显得你的思考更有深度。我个人在带团队和准备面试时的体会是TCP三次握手和四次挥手这题千万不要只背那几行流程。真正值钱的是理解每一次握手、每一次挥手背后的“为什么”——为什么是三次、为什么是四次、为什么有TIME_WAIT、为什么会有CLOSE_WAIT。把这些想透了再去翻Python的socket文档你会发现每个接口的设计都豁然开朗线上排查故障时也能直接定位到内核状态和代码问题而不需要靠运气瞎试。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →