ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

把TCP三次握手讲成网恋奔现,面试直接稳了

把TCP三次握手讲成网恋奔现,面试直接稳了 上个月去面一家做网络设备的中厂技术面第二轮面试官是个看起来三十出头、话不多的老哥。他翻了翻简历抬头问了一句“TCP三次握手为什么不设计成两次”我脑子里条件反射冒出来的是RFC 793、SYN队列、ISN随机化这些词但当时也不知道哪根筋搭错了张口就说“这就像网恋奔现为什么不能女生单方面确认男生喜欢她就直接奔现非要男生再确认一次”然后我就用网恋奔现的完整流程把三次握手、四次挥手、SYN Flood、TIME_WAIT全串了一遍。对面沉默了几秒嘴角动了一下接着问了下一个问题。这篇东西就是当时那套讲法的完整整理版。写给两类人一类是准备面试、被TCP状态机和各种报文段折磨得头大的求职者另一类是工作中要排查网络问题但TCP原理一直停留在“背过就忘”状态的后端、运维、嵌入式工程师。我会把网恋奔现这个故事从头讲到尾再切回专业术语顺便把面试官常追问的几个点全拆开揉碎。1. 为什么面试官爱问三次握手网恋奔现又凭什么能讲清楚1.1 三次握手的本质是“双向信道确认”先想一个问题面试官问三次握手到底在问什么很多人以为是考记忆力把SYN、SYN-ACK、ACK这三个报文背出来就完事。不是的。真正有经验的面试官问这个问题看的是三件事第一你知不知道三次握手解决的本质问题是什么第二你能不能把抽象概念用通俗方式讲清楚第三你愿不愿意往深处走一步讲到序列号、状态转换、异常处理这些底层机制。那三次握手解决的本质问题到底是什么就是一个非常朴素的需求通信双方要确认彼此既能发、又能收。这个需求放到生活里就是你跟一个人聊天对方到底在不在、能不能听到你说话、你说话他听不听得懂你得确认。TCP是面向连接的可靠传输协议在正式传输数据之前必须先建立一个“双方都确认无误”的信道。这个信道必须是双向的因为TCP连接建立之后数据是双向流动的客户端要往服务端发服务端也要往客户端发。如果你只确认了“我能发、你能收”但没确认“你能发、我能收”那后半段通信就是盲的。所以三次握手的核心不是三个包来回传一下那么简单而是通过三轮交互让双方把“我的发送能力”“你的接收能力”“你的发送能力”“我的接收能力”这四个维度全部确认一遍。1.2 网恋奔现和TCP连接的完整映射现在把场景切换成网恋奔现。假设男孩是客户端女孩是服务端。两个人网聊了很久男孩决定主动提见面。但注意这里的“见面”不是直接见面而是先通过消息确认双方意愿TCP连接的本质也是先通过控制报文确认双方状态再开始传输数据。第一次握手男孩发消息“我特别喜欢跟你聊天这周末我能去你城市找你吗”这句话在TCP里就是SYN报文意思是我要建立连接我有话想说。第二次握手女孩回复“我也觉得你挺好的周末我有空你来吧。”这句回复同时完成了两个任务一是告诉男孩“我收到你的消息了”二是表达“我也愿意跟你见面”。这就是SYN-ACK报文。第三次握手男孩回复“好嘞那周六下午三点车站见。”女孩收到这条消息后悬着的心才放下因为她确认了男孩确实收到了自己的回复。到这一步两个人才算真正约定好了见面。TCP连接也是一样只有三次握手全部完成客户端和服务端才双双进入ESTABLISHED状态才开始传业务数据。对应关系整理成一张表方便对照记步骤网恋场景TCP报文含义第一次男孩发邀约SYN1, seqx客户端请求建立连接第二次女孩同意并回应SYN1, ACK1, seqy, ackx1服务端确认收到并同意连接第三次男孩再确认ACK1, seqx1, acky1客户端确认收到服务端回复完成双方达成约定进入ESTABLISHED连接建立开始传数据这么一映射你再看三次握手的报文就不再是三个冷冰冰的标记组合了而是一个完整的故事。2. 三次握手逐包拆解SYN、SYN-ACK、ACK背后到底在干什么2.1 第一次握手客户端发起SYNseq值为什么是随机的第一次握手客户端向服务端发送SYN报文标志位SYN1同时携带一个序列号seqx。很多人初学TCP的时候会问这个seq到底怎么定的为什么不能从0开始非要随机先说答案seq是初始序列号ISNInitial Sequence Number必须是随机的。这在RFC 793里就规定了ISN要随时间变化每4微秒加1现代Linux实现还加入了随机偏移。这么设计的原因至少有两条。第一防序列号猜测攻击。如果ISN固定可预测攻击者就能伪造一个合法序列号的报文插入到正常的TCP连接里把数据篡改掉。随机化之后攻击者猜中序列号的成本大幅提高。第二防止旧连接的延迟报文干扰新连接。TCP报文在网络里可能被延迟如果客户端上一次连接用了同样的序列号一个网络里滞留了很久的旧数据包到了新连接里可能会被误认为是有效数据。序列号随机化之后新旧连接之间的序列号空间几乎不可能重叠。放到网恋故事里男孩发出去的那条邀约消息不是用固定模板发的而是每次都要带上一个只有他自己知道的随机标识。女孩之后回复的时候必须带上这个标识男孩才能确定她说的是“回复我的那条消息”而不是别人发的另外一条。2.2 第二次握手服务端回SYNACK一条消息干两件事服务端收到SYN报文之后会做两件事为这个连接分配资源创建传输控制块TCB然后回复SYNACK报文。注意这个报文的关键点它同时把SYN和ACK两个标志位都置1了这也是很多人看抓包的时候容易疑惑的地方——为什么第二次握手有两个标志位因为服务端在这一个报文里要传达两层信息。ACK1是对客户端SYN的确认告诉客户端“我收到你的连接请求了”SYN1是服务端自己的连接请求告诉客户端“我也想跟你建立连接”。换句话说服务端把自己的“同意”和“请求”合并成一条消息发出去了。为什么能合并因为客户端发送SYN之后服务端已经确认了“客户端能发、自己能收”所以服务端的确认消息可以安全送达客户端而服务端自己发起的“请求连接”消息正好可以搭上这趟车。报文里还有一个关键字段ackx1。这里特别容易搞混。有人会把ack理解成“我收到了x这个序列号”但TCP的确认号语义是ackx1表示“我已经收到了你发来的序列号x的报文我希望你下一次发送的报文序列号从x1开始”。所以ack填的是对方seq加1而不是原样返回。回到网恋故事女孩的回复“我收到你消息了我也愿意”本质上也是一条消息完成两件事。现实中如果你只回复“收到”没表达自己的意愿男孩还得再问一次“那你愿不愿意”。TCP把这两件事合并成一次回复省了一轮交互时间。2.3 第三次握手客户端回ACK连接才算真正建立第三次握手客户端收到服务端的SYNACK后会发送一个ACK报文标志位ACK1seqx1acky1。这个报文发出去之后客户端进入ESTABLISHED状态服务端收到这个ACK之后也进入ESTABLISHED状态。很多初学者会忽略一个关键点为什么第三次握手是必须的服务端发了SYNACK之后不能直接认为自己已经连接成功吗答案是不能。因为服务端发出SYNACK之后它不知道自己这条消息到底有没有被客户端收到。万一这条消息在网络里丢了服务端这边干等着客户端那边也干等着两个人都以为对方没有回应连接就卡住了。第三次握手就是客户端给服务端吃的定心丸“你的回复我收到了我们正式开始吧。”用网恋来类比就是女孩发出“我周末有空你来吧”之后假如这条消息没有被男孩收到女孩以为已经约好了周末打扮得漂漂亮亮去车站结果男孩根本没来。所以男孩收到消息之后回复“好嘞周六见”这个“确认的确认”是必要的。再往前走一步第三握手的ACK报文里seqx1和acky1也有讲究。seqx1是因为客户端上一次发送的SYN报文序列号是x现在确认收到服务端的SYN后客户端的数据序号自然从x1开始acky1则是告诉服务端“我已经收到了你序列号为y的报文你下次发数据从y1开始吧”。到这一步双方的初始序列号都完成了交换后续数据报文的编号规则就此确定。3. 面试官追问环节为什么是三次不是两次或四次3.1 两次握手会带来“历史重复连接”问题这个话题几乎是面试官的必问延伸题既然三次握手看起来多了一次确认那为什么不用两次省一次交互不是更快吗如果只从“确认双向收发能力”的角度看两次握手理论上也能把四个维度确认得差不多。但TCP的工程师考虑的不是理想情况而是网络环境里最糟糕的情况。最经典的反例就是历史重复连接请求。假设客户端给服务端发了一个SYN报文序号为100结果这个报文在网络里被堵了很久客户端迟迟没收到响应超时之后重新发了一个新的SYN报文序号为300。服务端收到序号300的SYN回复SYNACK客户端回应ACK第二次建立的连接正常工作。问题来了如果这时候那次被堵住的旧SYN序号100终于到达了服务端会发生什么在两次握手的机制下服务端收到这个旧SYN会认为客户端又想建立一条新连接于是回复SYNACK并且为这条连接分配资源进入ESTABLISHED状态。但客户端根本没有发起这次连接请求收到服务端回复后也搞不清楚状况。结果就是服务端凭空多了一条半吊子连接网络资源被白白占用严重的时候还可能导致数据错乱。三次握手就不会有这个问题。客户端如果收到对旧SYN的SYN-ACK响应一看ack值不对比如自己当前期望的下一个序列号是301结果收到的确认号是101立刻知道这是旧连接的残留报文于是发送RST报文重置连接服务端收到RST后释放资源。整个过程干净利落。所以三次握手和两次握手的本质区别在于第三次握手让客户端有机会对“服务端的响应”做一次“身份校验”把历史遗留的重复连接请求挡在门外。3.2 三次握手如何用序列号挡住迟到的旧请求上面说的“ack值不对”具体是怎么判断出来的这就是序列号机制的功劳。TCP连接建立之后通信双方各自持有两个序列号一个是自己发送数据的序号seq一个是期望收到对方数据的序号ack。每发送一个报文seq按数据长度递增每收到一个报文ack按对方的seq加数据长度更新。在三次握手过程中客户端收到了服务端回应的SYN-ACK会检查这个SYN-ACK报文的ack字段是否等于自己的初始序列号加1。如果等于说明这个响应是针对自己最新那次SYN的如果不等于说明这不是自己最新连接的响应要么是旧连接残留的要么是网络里被篡改过的客户端直接丢弃或者发送RST。这个机制翻译成人话就是男孩发出多条邀约消息之后女孩回复的时候必须带上自己最新那条消息的编号男孩才能确认她回的是“最近这次”而不是“上上周那次”。如果女孩回复的是旧编号男孩肯定要警觉不会傻乎乎地继续推进。这也是为什么第三次握手不能省略的原因之一没有这轮确认客户端就无法校验服务端的响应服务端也就无法排除旧连接请求的干扰。3.3 异常场景握手丢包、半连接、SYN Flood面试官如果对你的基础满意大概率会继续加码追问各种异常情况。这块内容建议面试前一定看完因为回答出来就是明显的加分项。第一种异常第一次握手丢了。客户端发了SYN迟迟没收到响应会触发超时重传机制。Linux下客户端SYN重传次数由tcp_syn_retries控制默认是6次初始超时时间大约是1秒之后指数退避总耗时大约127秒。如果重传6次之后还是没有收到SYN-ACK客户端放弃返回连接超时错误。第二种异常第二次握手丢了。客户端等不到SYN-ACK同样触发超时重传重新发SYN。此时服务端已经发出了SYN-ACK会进入一个等待状态。注意服务端在发出SYN-ACK之后并不是什么都不干它也会启动重传定时器等待客户端的ACK。如果等不到服务端会重传SYN-ACK重传次数由tcp_synack_retries控制默认5次。第三种异常第三次握手丢了。客户端已经进入ESTABLISHED状态但服务端一直没收到ACK于是服务端会反复重传SYN-ACK。重传次数耗尽后服务端关闭这个半开连接释放资源。这时候如果客户端向服务端发送数据服务端会返回RST报文客户端就会发现连接异常。再往深走一步就是SYN Flood攻击。攻击者伪造大量IP地址向服务端发送SYN但不回复第三次握手。服务端每收到一个SYN就会在半连接队列里为它分配资源半连接队列是有限的队列被打满后正常的连接请求也无法处理这就是典型的拒绝服务攻击。现代系统应对SYN Flood一个关键手段是SYN Cookie。简单说服务端收到SYN后先不分配资源而是把连接的关键信息加密编码成一个Cookie作为序列号发回去只有收到客户端的ACK并验证Cookie通过后才真正分配资源。这样即使收到大量伪造SYN服务端也不会被榨干内存。这块内容对应到网恋场景就是女孩一天收到几千条“我喜欢你”如果不加筛选全部认真回复人早就累垮了。所以她要先用一个小本子记下“已回复的邀约”只有男孩确认收到回复之后才真正认真交往。4. 从三次握手延伸到四次挥手把TCP生命周期讲完整4.1 分手阶段四次挥手拆解面试官问完三次握手至少有六成概率会接着问四次挥手。这俩是同一个生命周期里的两件事建议一起准备。四次挥手的过程用网恋分手来类比特别合适。男孩女孩在一起一段时间发现不合适决定分手。TCP连接关闭也是两个人“双向确认”的过程而且因为各自可能有未发送完的数据两个方向的关闭需要分别完成。第一次挥手客户端发送FIN报文进入FIN_WAIT_1状态。对应网恋场景就是男孩发消息“我们分手吧就这样。”注意这只是一个“我想关闭连接”的请求不代表数据传输立刻停止。男孩说了分手之后他可能还有些话没说完。第二次挥手服务端收到FIN回复ACK进入CLOSE_WAIT状态。对应女孩回复“我收到你的意思了。”女孩表示“我知道你想分手了”但女孩自己可能还有一些数据要发给男孩所以连接还没有完全关闭。客户端收到ACK后进入FIN_WAIT_2状态。这里就体现了为什么挥手是四次而不是三次TCP是全双工的每个方向都需要独立关闭。男孩主动提分手只能代表“我不再发数据了”但没有权利替女孩决定“你也不能再发数据”。所以女孩要先确认收到分手请求然后继续把自己剩下的数据发完。第三次挥手服务端把剩余数据发送完毕发送FIN报文进入LAST_ACK状态。对应女孩把自己最后的话说完才表达“我也同意分手这段关系到此为止。”第四次挥手客户端收到FIN回复ACK进入TIME_WAIT状态。对应男孩收到女孩的信息回复“好我知道了。”到这一步两个方向的关闭都确认完毕。有人可能会问第三次和第二次能不能合并也就是服务端收到FIN后直接回FINACK理论上如果服务端在收到FIN时已经没有数据要发送确实可以合并。很多教科书也提到“一般情况下是四次但某些场景可以合并成三次”。面试时如果主动说出这个细节会显得理解更深入。4.2 TIME_WAIT为什么等待2MSL能不能省掉四次挥手最后有个细节特别容易被忽略也特别容易被追问客户端在发送最后一次ACK之后会进入TIME_WAIT状态等待2MSL后才真正关闭连接。为什么要等这么久两个原因。第一个原因是确保最后的ACK能到达服务端。客户端发出的最后一次ACK有可能会丢如果丢了服务端那边一直等不到确认会重传FIN。客户端必须保留足够的时间以便收到重传的FIN之后重新发送ACK。如果客户端收到FIN后立刻关闭服务端重传的FIN就得不到响应服务端永远无法进入关闭状态。第二个原因是让本次连接产生的所有迟到报文在网络中自然消亡。MSL是报文最大存活时间网络中几乎不会出现存活超过2MSL的报文。等待2MSL之后本次连接的所有残留报文都已经消失新的TCP连接即使使用完全相同的四元组源IP、源端口、目的IP、目的端口也不会收到上一代连接的脏数据。对应到网恋故事男孩发出最后一条“好我知道了”后没有立刻删掉女孩的微信而是保留一段时间确认女孩真的收到了这条消息。如果女孩没收到她还会再联系男孩保留的这段时间就是为了兜底处理这种情况。TIME_WAIT在工程上是个热门话题。大量短连接的服务端或者客户端经常会出现TIME_WAIT堆积导致可用端口耗尽。Linux下的Time_wait复用机制tcp_tw_reuse只对发起连接的一方生效而tcp_tw_recycle因为精度问题在Linux 4.12之后已经被移除。所以实践中更稳妥的方式是调整服务端keepalive策略、优化连接池、或者把短连接改成可复用的长连接。这块内容在实战中特别常见比如你启动一个服务报错“error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address”大概率是端口被TIME_WAIT状态或者另一个进程占用。遇到这种问题先netstat查一下端口状态确认占用方再决定是等TCP等待超时还是启用SO_REUSEADDR。SO_REUSEADDR允许新启动的进程绑定处于TIME_WAIT状态的端口但前提是新进程不是处于LISTEN状态的重复监听。5. 面试实战怎么把“网恋比喻”讲成加分项5.1 面试官真实考察点状态转换、报文段、字段用比喻讲TCP三次握手最大的风险是讲完故事就被面试官误以为你只会讲故事。所以比喻只是第一步后面必须切回专业术语把面试官关心的硬核考点全部覆盖到。面试官通常关注的几个硬核考点我列一下第一状态转换。三次握手涉及的状态有CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED。客户端从CLOSED进入SYN_SENT收到SYN-ACK后进入ESTABLISHED服务端从LISTEN进入SYN_RCVD收到ACK后进入ESTABLISHED。这五个状态必须能脱口而出。第二报文段的关键字段。SYN、ACK标志位的组合seq和ack的计算方式ISN的随机化原因。把这三个字段讲透面试官基本能确认你理解TCP的确认机制。第三为什么不是两次。这个是高频追问参照第3章的内容回答即可。关键要说到“历史重复连接请求”这是区分“背过”和“理解”的分水岭。第四握手和连接建立的超时、重传逻辑。tcp_syn_retries、tcp_synack_retries、半连接队列、SYN Cookie这些是拉开差距的内容。我在面试里通常的节奏是先用比喻把整个流程讲一遍让面试官跟着故事走然后立刻说“这是用生活场景帮助理解回到协议层面三次握手的本质是确认双方收发能力并交换初始序列号”接着主动补上历史重复连接问题最后看面试官表情如果他感兴趣再往SYN Flood和半连接队列延伸。整套下来面试官基本能判断你不只是背了八股文。5.2 一套可以直接用的现场回答话术很多人在面试时知道大概意思但一开口就逻辑混乱。这里给你一套我实测过的话术框架照着结构练不用死背“TCP三次握手的本质是让通信双方确认彼此的收发能力同时交换初始序列号。客户端先发送SYN报文seq设为随机初始值x表示我想建立连接服务端收到后回复SYNACK报文ackx1seq设为y表示我收到了你的请求并且我也同意建立连接客户端再发送ACK报文seqx1acky1表示我也收到了你的同意。经过这三轮客户端和服务端都确认了对方能收能发连接建立双方进入ESTABLISHED状态。至于为什么必须是三次而不是两次核心原因是防止历史重复连接请求干扰通信。网络里可能出现一个迟到的旧SYN报文如果在两次握手机制下服务端收到旧SYN后会直接分配资源并进入ESTABLISHED但客户端根本没有发起这条连接导致资源浪费和数据错乱。三次握手时客户端收到对旧SYN的SYN-ACK响应发现确认号不是自己期望的值会发送RST终止连接服务端随之释放资源。”这段话术的好处是前半段讲清楚机制后半段亮出核心理解逻辑闭环没有废话。5.3 我在实际面试和项目里踩过的坑最后分享几个实战中的心得算是我自己踩过的坑。第一个坑只讲故事不切回专业术语。我第一次给人讲“网恋奔现”类比的时候讲得眉飞色舞但对方问了一句“那seq和ack具体怎么算”我一下子卡住了。后来我把所有比喻都做成“故事术语对照”每次讲完故事都会强制自己把对应的报文格式写一遍。面试也是一样故事是引子硬功底才是主菜。第二个坑只提三次握手不准备四次挥手。有次面试官在三次握手讲完后顺嘴问了一句“关闭连接呢”我当时只记得FIN和ACK两个词状态转换完全说不清楚场面一度很尴尬。所以建连接和断连接一定要一起准备两者逻辑是互通的。第三个坑忽略实战排查经验。面试官很喜欢问“你在工作中遇到过TCP问题吗”。我面试的时候举过一个真实案例Docker启动容器时直接报“error response from daemon: ports are not available”当时排查发现是宿主机上某个进程占用了端口并且还有一堆TIME_WAIT状态的连接堆积。这个案例既展示了排查思路又体现了对TCP状态的掌握。第四个坑讲比喻的时候用词不够准确。比如“第三次握手是确认收到服务端的确认”这个说法容易被人误读成“确认的确认”是多余的。准确的表达是第三次握手告诉服务端“我收到了你的SYN-ACK”让服务端能够从不确定状态转入确定状态这是建立双向可靠信道闭环的必要一环。面试结束后那个老哥没有再追问网络题直接进入了项目环节。后来我复盘觉得整场面试最出彩的部分就是那段网恋奔现的比喻因为它把抽象概念具象化了也让面试官记住了我这个人。TCP三次握手本身不难难的是把它讲得让外行也能理解同时让内行觉得你有深度。我个人现在的习惯是给团队新人讲TCP一定先用网恋奔现把整个流程过一遍然后再带他们去看Wireshark抓包把SYN、SYN-ACK、ACK三个报文对应到故事里去。等他们自己抓到包看到那三个标志位的变化理解立刻就不一样了。如果你也在准备面试建议你也找一个自己熟悉的场景把三次握手、四次挥手完整讲一遍。能讲明白你就真的学会了。讲不明白的地方就是你需要补课的地方。
返回列表