45 分钟
部署与基础设施工程

TCP 与 UDP:可靠是怎么来的,三次握手到底在握什么

讲透传输层两大协议:TCP 如何用序列号、确认、重传换来可靠,为什么建立连接要三次握手、断开要四次挥手,UDP 又为什么敢「不可靠」却更快,以及这些原理如何影响 App 的网络体验

  • 理解传输层职责,知道 TCP 的可靠、有序、面向连接是用什么机制换来的
  • 能讲清三次握手、四次挥手每一步在做什么,以及为什么是三次和四次
  • 分清 TCP 与 UDP 的适用场景,知道 DNS、音视频、HTTP/3 各自选了谁
  • 把连接原理映射到真实工程:keep-alive、连接池、弱网重传、流式返回

同样是发数据,为什么要分 TCP 和 UDP

上一课说传输层负责「进程到进程」的通信。这一层有两个主角:TCP(传输控制协议)和 UDP(用户数据报协议)。你每天用的网页、HTTPS 接口、文件下载几乎都走 TCP;而域名解析、视频通话、直播、在线游戏大量用 UDP。它们的差别不是「谁更先进」,而是在「可靠」和「快、简单」之间做了不同取舍。理解这个取舍,你才看得懂连接为什么慢、弱网为什么转圈、流式输出为什么那样设计。

TCP:用一套复杂机制,换「可靠且有序」

IP 层只负责把数据包尽力发出去,不保证不丢、不保证顺序、不保证不重复。TCP 在其上建立了一套承诺:数据无差错、不丢失、不重复、按发送顺序到达,这叫面向连接的、可靠的字节流。它靠几块基石实现:

TCP 可靠性的四块基石序列号 :给每个字节编号,接收方据此排序、发现丢包确认ACK :接收方告诉发送方「我连续收到哪了」超时重传:发出去一段时间没收到 ACK,就认为丢了、重发一遍流量/拥塞控制:根据接收方能力和网络拥堵程度动态调整发送速度,避免把网络压垮

代价是额外开销:建立连接要握手、每个包带序号和确认、丢包要等待重传。所以 TCP 适合「一个字节都不能错」的场景——网页、API、文件、数据库连接;不适合「宁可丢一两帧也不能卡」的实时语音。

三次握手:为什么偏偏是三次,两次不行吗

TCP 是面向连接的,传数据前要先在双方之间建立连接,这个建立过程固定是三次报文,俗称三次握手:

示例
客户端                          服务器
  | --- SYN(我想连,我的初始序号x)----> |   第1次
  | <-- SYN,ACK(同意,我的序号y,确认x)-- |   第2次
  | --- ACK(确认y,连接建立)---------> |   第3次
  | ========= 之后才开始传数据 ========= |

关键问题:为什么不能两次?因为建立连接要确认两件事——双方各自的「发送能力」和「接收能力」都正常,并且要让双方都同步好彼此的初始序列号。第 1 次客户端证明自己能发;第 2 次服务器证明自己能收也能发;第 3 次客户端再确认自己能收到服务器的回应。少了第三次,服务器无法确认客户端是否真的收到了自己的同步信号;更重要的是,网络中可能滞留一个早已过期的连接请求,两次握手会让服务器仅凭这个旧请求就白白建立连接,三次握手则让客户端在第三次有机会否决它。三次,是确认双向通道全通所需的最少次数。

选学

连接与断开时的状态机(选学)

TCP 连接在操作系统内核里是有状态的,理解几个经典状态有助于排查部署问题:

握手:CLOSED -> SYN_SENT(客户端发 SYN)-> ESTABLISHED(双方建立)挥手:ESTABLISHED -> FIN_WAIT -> ... -> TIME_WAIT -> CLOSEDLISTEN:服务器正在等待别人来连(服务正常监听时应看到它)ESTABLISHED:连接已建立、正在通信TIME_WAIT:主动关闭一方进入,等待约 2 倍 MSL 后才彻底关闭

部署时用 ss -tanl 或 netstat 能看到这些状态:服务起没起看有没有 LISTEN,连接多不多看 ESTABLISHED,服务器上堆积大量 TIME_WAIT 通常是它主动短连太多的信号。

四次挥手与 TIME_WAIT:断开为什么反而更麻烦

TCP 是全双工的——两个方向可以独立收发数据,所以关闭时两个方向要分别关闭,通常需要四次报文:

示例
主动方                         被动方
  | -- FIN(我发完了)--------------> |        第1次
  | <-- ACK(知道了)--------------- |        第2次(此时被动方可能还有数据要发)
  | <-- FIN(我也发完了)------------ |        第3次
  | -- ACK(好,再见)-------------> |        第4

第 2 次的 ACK 和第 3 次的 FIN 之所以分开,是因为收到对方「我发完了」时,自己这边可能还有没发完的数据,要先发 ACK 应付着、等数据发完再发 FIN。主动关闭的一方发完最后一个 ACK 后会进入 TIME_WAIT,停留约 2MSL(最长报文寿命的两倍)才彻底释放,目的有二:确保最后的 ACK 万一丢失还能重发、让本次连接在网络中残留的旧报文自然消亡,避免它们污染下一条复用同一端口的新连接。部署启示:频繁重启的服务可能因 TIME_WAIT 占用而一时无法重新绑定端口,服务器程序通常会开启 SO_REUSEADDR(地址复用)来缓解。

UDP:为了快和简单,主动放弃可靠

UDP 走另一个极端:无连接(不握手、挥挥手就发)、不保证到达、不保证顺序、没有拥塞控制,每个数据报独立、头部极小(只有 8 字节)。它把「要不要重传、要不要排序」的自由交还给应用自己。正因为省去了握手和重传等待,UDP 延迟极低,适合「实时性比完整性更重要」的场景:

配对题场景与更合适的传输层协议配对
ℹ️为什么 HTTP/3 反而基于 UDP

TCP 诞生早,它的一些机制(比如一个丢包会卡住同连接里所有请求的「队头阻塞」、握手和 TLS 握手分开跑导致建连慢)在今天成了瓶颈。HTTP/3 用的 QUIC 干脆跑在 UDP 上,由应用层自己实现更灵活的可靠传输、把传输握手和加密握手合并,在移动网络切换基站时连接还能不中断。这说明选 TCP 还是 UDP 不是信仰,而是看你要什么保证。

这些原理如何影响 App 的真实网络体验

传输层不是面试题,它直接决定你接口该怎么设计。第一,握手有成本,所以不要每个请求都新建连接:HTTP 的 keep-alive 让一条 TCP 连接承载多个请求,后端到后端、后端到数据库则用连接池复用。第二,弱网下 TCP 的超时重传会表现为「转圈很久」,因此客户端要设合理的超时与可重试机制,且重试接口必须幂等(同一请求重复执行结果不变)。第三,像大模型逐字输出、TTS 流式播放这种「边生成边返回」的场景,朋友好学用的是 SSE(Server-Sent Events):它建立一条长连接(跑在 TCP 上),服务器生成一点推一点,而不是让用户干等整段生成完——这正是利用了 TCP 有序字节流的特性。

⚠️部署里最常见的三个连接误区

短连接风暴:每个请求都重新握手,高并发下服务器堆满 TIME_WAIT、延迟飙升,要用 keep-alive/连接池;不幂等的重试:网络超时就盲目重发「扣款/下单」类请求,可能执行两次,必须让这类接口带唯一请求号、服务端去重;把流式当轮询:本可以一条 SSE 长连接逐块返回,却用客户端每隔 1 秒轮询一次,白白浪费连接和电量。

🐍资深杂谈:接口设计其实是在为网络特性买单

你往后设计任何接口都会遇到这对矛盾:网络既不可靠又有延迟。于是有了幂等(容忍重传)、有了分页与字段裁剪(减少传输量)、有了压缩和缓存(少传)、有了连接复用(少握手)、有了超时/熔断/降级(不被慢响应拖垮)。这些看似是「后端规范」,根子都在传输层的客观限制上。看懂 TCP/UDP,再看这些工程实践就不是死记硬背,而是顺理成章。

选择题

关于 TCP 三次握手,下列说法正确的是?

本节小结

传输层在 IP 之上做进程到进程通信。TCP 面向连接、可靠有序,靠序列号、ACK、超时重传、流量与拥塞控制实现,适合网页/API/文件,代价是握手与重传开销;三次握手同步双方序号、确认双向能力并防旧连接,是最少次数;四次挥手因全双工需双向分别关闭,主动方 TIME_WAIT 停留 2MSL 以保证可靠关闭、消散旧报文,频繁重启的服务需 SO_REUSEADDR。UDP 无连接、不保证可靠有序、头部极小、延迟低,适合 DNS、音视频、游戏,HTTP/3 的 QUIC 基于 UDP 自建可靠与加密。工程上映射为:keep-alive/连接池减少握手、接口幂等以容忍重传、SSE 长连接实现大模型与 TTS 的流式返回。

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

把会话、上传文件、定时任务状态从应用进程里挪到 Redis、对象存储或数据库后,任何一台应用实例都能处理任何请求,扩容就是多开几个实例挂到负载均衡后面。反过来,只要状态留在某台机器的内存里,它就既无法横向扩容,也无法滚动更新(一重启用户就掉线)。面试答「如何支撑更高并发」,先讲无状态化,再谈加机器和缓存。