ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TCP三次握手深入剖析:从原理到工程的完整理解

TCP三次握手深入剖析:从原理到工程的完整理解 提到TCP三次握手我第一反应不是“SYN、SYNACK、ACK”而是“面试官到底想听什么”。很多人把三次握手的流程背得滚瓜烂熟但一旦被追问“为什么不能是两次”“第三次ACK丢了会怎么样”立刻卡壳。这个问题之所以经典是因为它考的不是记忆而是对TCP连接本质的理解。写这篇文章就是想把三次握手背后的三个核心问题讲透它确认了什么、同步了什么、防御了什么以及这套机制在真实网络编程里的延伸影响。无论你是准备面试的开发者还是在排查连接的坑都应该吃透这一块。1. 三次握手到底在解决什么问题三次握手不是简单的“打招呼”它本质上是两个端点之间进行的一次状态协商。TCP是一个面向连接的可靠协议“连接”这两个字不是虚的它意味着收发双方需要维护一些共同状态才能正确地把数据字节流传送给对方。在连接建立之前双方对彼此一无所知通过三次交换至少要达成下面三个子目标。1.1 三个子目标能力确认、序列号同步、防历史复发第一个子目标是能力确认。逻辑上双方需要确认“我能发你也能发我能收你也能收”。这听起来简单但在分布式网络中一个消息从A发出B能否正确收到在收到ACK之前是无从得知的。所以需要一方发探测消息另一方回确认再让发起方对确认做二次确认形成一条完整的确认链。第二个子目标是序列号同步。TCP每个方向都有自己的字节流序列号接收方需要知道发送方的初始序列号ISN才能做排序、去重、累计确认。ISN是随机选择的如果不做同步双方都不知道对方下一个字节从哪个序号开始后续所有数据包都无法对应。第三个子目标是防止历史重复连接。网络环境不是理想状态一个很久以前发出的SYN报文可能因为延迟突然出现在服务器门口。如果服务器收到一次SYN就直接认定连接建立那么这个“僵尸请求”就会白白占用资源甚至干扰一条已经存在的连接。三次握手通过客户端最后一次ACK来表达“当前这个连接是我真实想要的”从而把历史SYN过滤掉。1.2 两次握手为什么一定不行很多人说“两次不行”但讲不清为什么。我们推演一下只有两次握手的情况客户端发SYN服务器收到后回SYNACK然后双方就算建立了连接。问题出在服务器永远无法确认客户端是否收到了自己的SYNACK。假设SYNACK在路上丢了客户端这边一直等不到回复会认为连接没有建立可能再次发起SYN重试。服务器却已经进入了ESTABLISHED状态开始分配资源、等待业务数据。这样一个连接客户端根本不知道它的存在服务器却在为它做着无谓的支撑。如果是客户端因为某种原因已经关闭了端口当服务器收到这种无法投递的后续数据时只能依赖RST去兜底而资源浪费已经发生了。再从序列号同步的角度看两次握手也有致命伤。第一次SYN携带了客户端的初始序列号第二次SYNACK携带了服务端的初始序列号此时客户端确实知道了双方的序列号但服务器并不知道客户端是否已经知道了自己的序列号。缺少一个针对服务端SYNACK的确认服务器后续发送数据时就无法确定客户端的接收窗口和ACK基准是否已经就绪。所以二次握手是信息不对称的握手总有一方在“盲猜”。三次握手刚好把这种不对称补上。2. 三次握手中每一步的角色与细节面试时把“SYN、SYNACK、ACK”说出来只是第一步每一步到底改了哪些状态、携带了什么信息才是加分项。2.1 三次交换与状态迁移下面这张表把三次握手过程中双方的状态变化列出来步骤发送方报文类型接收方状态变化同时携带的信息第1次客户端SYNseq x服务端LISTEN - SYN_RCVD客户端的初始序列号x第2次服务端SYNACKseq y, ack x1客户端SYN_SENT - ESTABLISHED服务端初始序列号y同时确认收到x第3次客户端ACKack y1服务端SYN_RCVD - ESTABLISHED确认收到服务端的序列号y注意这里的序列号都使用了相对编号实际抓包中seq是绝对随机数比如4000000000。第1次SYN中的seqx代表客户端发送数据的第一个字节的序号第2次SYNACK中的ackx1表示“我已经收到你seqx的报文期望你下一个报文从x1开始”。第3次ACK中的acky1同理是对服务端SYN的确认。2.2 为什么客户端最后一个ACK是“和事佬”第三个ACK不携带任何应用数据往往被忽略但它的角色非常关键。服务端在发出SYNACK后立刻面临一个不确定性我这条消息到底到没到如果没到我盲目进入ESTABLISHED状态后续发数据客户端可能会因为没收过SYNACK而无法正常解析。所以服务端必须等一个针对SYNACK的确认。这个ACK一旦发出并到达服务端服务端心里那块石头才落地至此双方的发送和接收通道都经过了完整确认连接才真正进入稳定可用的状态。有个好记的说法第一次握手是客户端说“我想建立连接”第二次握手是服务端说“我收到你的请求我也准备好给你发数据了你能收到吗”第三次握手是客户端回一句“我能收到我也确实要和你建立连接”。话虽通俗但道出了双方信息对齐的本质。2.3 报文在抓包里长什么样我们起一个tcpdump就能清清楚楚看到这个过程。假设客户端ip 192.168.1.10用端口56789访问服务端192.168.1.20的80端口14:58:31.123456 IP 192.168.1.10.56789 192.168.1.20.80: Flags [S], seq 4000000000, win 64240, length 0 14:58:31.123489 IP 192.168.1.20.80 192.168.1.10.56789: Flags [S.], seq 1000000000, ack 4000000001, win 65535, length 0 14:58:31.123500 IP 192.168.1.10.56789 192.168.1.20.80: Flags [.], ack 1000000001, win 65496, length 0Flags[S]表示SYN置位[S.]表示SYN和ACK同时置位[.]表示仅ACK置位。这正是三次握手最容易辨认的形态。这里seq都写得很大因为TCP初始序列号是随机的防止外部猜测序号去伪造报文。第二个报文中ack4000000001等于第一个报文的seq1。第三个报文中ack1000000001等于第二个报文的seq1。这个1是因为SYN报文会占用一个序列号即使它不携带数据。3. 为什么“三次”是数学上的最优解很多面试官会追问为什么不能是四次五次答案要从分布式确认的“最少次数”去理解。3.1 最少必要次数一次握手不用多说连基本方向都确认不了。两次握手的问题在第1节已经剖析无法让服务器确认“客户端的接收能力”。三次握手让双方都完成一轮“发送请求、收到确认、确认对方已收到确认”的闭环。可以这样算要让双方都确定“对方已经知道我的初始序列号”需要几次握手第一次客户端让服务器知道了自己的ISN第二次服务器让客户端知道了自己的ISN第三次客户端告诉服务器“我已经知道你的ISN”。到这一步服务器的ISN信息在两端达成一致而客户端的ISN从第一次握手开始就是已知的其实第二次的ack也在向客户端确认“服务器知道我的ISN”。所以三次足够把两个ISN都同步好。四次行不行也行但多出来的第四次交换并没有带来新的协议信息。所有需要同步的状态在第三次时已经全部对齐。多一次只是增加握手开销和时延纯属浪费。TCP是讲究效率的协议能用最低成本达成就绝不加戏。3.2 历史重复SYN的防御RST的作用三次握手防历史重复请求的机制值得单独拿出来讲。假设一台客户端曾经向服务端发送过SYN但这个SYN在网络中滞留了很久。期间客户端已经超时重传并成功建立了新连接然后数据传输完毕连接关闭。此时那个旧的SYN才姗姗来迟。如果采用两次握手服务端收到旧SYN会直接分配资源、进入ESTABLISHED然后开始期待从该客户端发送数据。问题是客户端已经完全不想要这个连接了于是这个“幽灵连接”就挂在了服务端直到超时回收期间服务端的内存、文件描述符、端口资源都被白白占用。恶意攻击者如果利用这个机制批量发送滞留SYN甚至可以直接把服务端的资源打满。三次握手之所以能解决是因为服务端不轻易相信一个SYN就建连。它回一个SYNACK后会等待客户端发来ACK。如果客户端没有当前连接意图收到SYNACK后会返回一个RST报文告诉服务端“我不认这个连接”。服务端收到RST后就会释放半开状态资源随之回收。这个一推一拉的过程完美过滤了历史残留。3.3 资源分配和SYN Flood带来的威胁三次握手在服务器端天然涉及两个队列半连接队列和全连接队列。客户端SYN到达服务端在SYN_RCVD状态时会把连接放入半连接队列第三次ACK到达后状态变为ESTABLISHED再从半连接队列迁移到全连接队列等待应用层accept。这个设计虽然解决了历史SYN问题但也诞生了SYN Flood攻击。攻击者可以故意不回第三次ACK只不断发送SYN让服务端半连接队列被占满后续正常的连接请求无法进队。实际防护手段里SYN Cookies是非常经典的一招在SYNACK时不再保持半连接状态而是把连接状态编码进cookie等客户端ACK回来再解码恢复。这也是理解三次握手“状态”价值的一个延伸握手状态可以被隐藏在cookie里未必一定要占用服务端内存。从协议层面看三次握手让连接建立这个动作的“代价”和服务端的“信任”达到了平衡既不会因为一次SYN就浪费资源也不会因为多次往返增加不必要的延迟。4. 从抓包和代码看三次握手的实战表现理论说再多不如动手看一眼。这一节我从实际编程和抓包的角度把三次握手和操作系统协议栈联系起来。4.1 用tcpdump实时看一次连接建立你可以在Linux机器上运行命令抓取自己发起的连接sudo tcpdump -i eth0 tcp port 80 -nn然后在另一个终端用curl访问一个网站curl -I http://example.com抓包里除了能看到TCP握手包还能看到HTTP请求随之而来。注意HTTP请求数据一定是在第三次ACK之后的包中发出不会出现“连接没建立就发数据”的情况。这个顺序本身就是TCP为应用层提供的可靠连接保证数据包和连接状态严格绑定。4.2 C语言socket编程中三次握手藏在哪里用C语言写过TCP服务的同学都知道服务端调用socket()、bind()、listen()后端口进入LISTEN状态。客户端调用connect()时内核会立刻发出SYN然后阻塞等待SYNACK收到后发出ACKconnect()才会返回成功。这个过程对应用层来说是透明的但内核里的状态机在飞速运转。服务端在accept()返回握手成功的套接字之前这个连接可能已经在内核态走完了三次握手进入全连接队列。如果应用层一直不accept连接会堆积在队列里导致客户端connect成功但服务端应用完全无感知。这种“握手已完成业务无法处理”的现象在高并发下很常见本质是TCP状态机和业务代码没有协同好。编程时容易忽视一点connect()是有超时的。SYN发出后如果一直收不到响应内核会按指数退避重传SYN。Linux下可以通过修改/proc/sys/net/ipv4/tcp_syn_retries调整重传次数默认通常是6次总耗时约1分多钟。所以一个connect()卡在那里几十秒不是程序卡死而是内核在等握手响应。sysctl net.ipv4.tcp_syn_retries这段话对排查“连接建立慢”的问题很有用你去抓包会看到SYN、SYNACK、ACK的间隔是否正常。如果只有SYN重传没有响应八成是服务端端口没监听或者防火墙把SYN丢掉了。4.3 握手成功后你可能踩的TIME_WAIT坑握手解决的是建立连接但连接一定会被关闭。关闭时四次挥手会产生TIME_WAIT状态然后出现经典报错Java客户端重连时报“Address already in use”。很多人不理解我客户端主动connect为什么本地端口会被占用原因很简单本地随机选了一个临时端口去connect连接关闭后进入TIME_WAIT这个端口和远端IP、远端端口构成的四元组会保留一段时间默认60秒具体由tcp_fin_timeout等参数影响。如果客户端在短时间内反复创建新连接又恰好随机到同一个源端口就会尝试bind一个正在TIME_WAIT中的地址于是触发Address already in use。解决办法不是改三次握手而是设置SO_REUSEADDRserverSocket.setReuseAddress(true);这个选项允许在TIME_WAIT期间复用本地端口。对于客户端频繁重连的业务更好的方案是使用连接池而不是反复建立短连接。这也是为什么很多数据库客户端、消息队列客户端都会维护连接池而不是每次请求新建连接。再说一句三次握手作为“连接创建”的成本在延迟敏感场景中不可小觑。一次握手至少要一个RTT跨地域长距离时RTT动辄上百毫秒如果业务频繁建连性能损耗肉眼可见。5. 面试高频追问与排查技巧这一节整理几个围绕三次握手的高频追问和实际排查场景希望帮你在面试和工作中都能对上号。5.1 第三次ACK丢失会发生什么这是最常见的追问。第三次ACK如果丢了服务端在SYN_RCVD状态下一直等不到ACK会按递增间隔重传SYNACK同时客户端已经进入ESTABLISHED状态开始发业务数据。服务端收到客户端的数据包时发现该数据包的ACK正好确认了自己的SYNACK它会将这个连接迁移到ESTABLISHED然后继续处理数据。所以第三次ACK丢失不一定导致连接建立失败只是多了一次重传同时可能让客户端早于服务端进入“连接已可用”状态造成短暂的半开窗口。但如果服务端一直没有收到任何来自客户端的数据或重传的ACK它会一直重试SYNACK直到超过tcp_synack_retries后放弃向客户端发送RST。客户端此时如果还在高高兴兴发数据就会收到RST包连接立刻被断开。面试时这样回答能体现出你对TCP状态机的颗粒度掌握而不仅仅是背流程。5.2 “已经建立的连接”和“半开连接”的区别三次握手建立的是完整连接。如果连接建立之后一端突然宕机另一端不会立刻知道这种状态叫半开连接。网上有人把它和半连接混淆。半连接是三次握手尚未完成、服务端处于SYN_RCVD的状态半开连接则是完成握手后链路中断但双方没有感知。排查半开连接时可以用Linux的ss命令看状态ss -t state established如果发现大量连接长时间没有任何收发可以依赖TCP的KeepAlive机制去探测。TCP KeepAlive默认关闭打开后会在空闲一段时间发送探测报文但这和三次握手没有直接关系属于连接保活的范畴。面试里区分开这两个概念同样很加分。5.3 面试时如何组织回答框架如果你在面试现场遇到“为什么TCP要三次握手”可以按三层框架答。第一层讲能力确认说明为什么最少要三次。第二层讲序列号同步说明SYN如何携带初始序列号以及ACK如何确认对方序列号。第三层讲历史重复连接防御和资源分配点明两次握手会让服务端盲目建连、产生资源浪费三次握手可以借助RST清理无效请求。每层都给出一个实际场景佐证比如SYN Flood或者TIME_WAIT。这个回答结构既能让面试官看到你的协议理解深度也能让你的思路始终围绕“解决了什么问题”展开。单纯背流程是拿不到高分的重点在于“为什么”。我自己带过不少新人发现能把三次握手讲透的人排查网络问题的时候思路往往特别清楚。因为他们知道TCP的一切机制背后都是为了解决分布式环境下的不确定性问题——报文可能丢、可能重、可能乱序也可能迟到。三次握手不是繁文缛节而是用最小的代价把连接建立的不确定性降到了最低。
返回列表