图解 TCP 三次握手与 UDP 无连接通信
从一次"传输失败"说起
学计算机网络的时候,TCP 和 UDP 这两个词几乎无处不在,但很长一段时间里,我对它们的理解都停留在"TCP 可靠、UDP 快"这种模糊的层面。真正让我想把它们彻底搞懂的契机,是上学期做网络调试工具(NDATools)的时候——我需要亲手处理 TCP 的连接、粘包、挥手,以及 UDP 的"发了就不管"。
那时候我才意识到,与其背概念,不如把每一个握手报文、每一个状态转换都"画"出来看一遍。这篇文章就是我当时梳理的结果,从 Socket 的五元组,到 TCP 的三次握手、可靠交付、四次挥手,再到 UDP 的无连接通信。文末还放了一个我自己做的交互式实验页面,你可以点进去亲自动手看动画。
Socket 与五元组:连接从哪里开始
不管是 TCP 还是 UDP,在写代码时都要先创建一个 socket。socket 可以理解成操作系统提供的一个"通信端点",它把复杂的网络协议栈封装成了一个像文件一样的对象——你能打开它、读写它、关闭它。
一个 socket 由五元组唯一标识:
五元组 = 源 IP + 源端口 + 目的 IP + 目的端口 + 协议(TCP/UDP)
这五个信息加起来,才能确定"谁在跟谁、用什么协议通信"。比如你打开浏览器访问一个网站,浏览器进程会拿到一个随机源端口,目的端口是 443(HTTPS),协议是 TCP。任何一个元素变了,都算另一条连接。
TCP 的三次握手:建立连接
TCP 是面向连接的协议,通信之前必须先建立连接。这个过程就是著名的"三次握手":
客户端 服务器
│ │
│── SYN (seq=x) ──────────────────→│ 第 1 次:我想连你,我的序号是 x
│ │
│←─ SYN+ACK (seq=y, ack=x+1) ──────│ 第 2 次:收到,我同意,我的序号是 y
│ │
│── ACK (ack=y+1) ────────────────→│ 第 3 次:好,连接建立
│ │
└────────── 连接建立,开始传数据 ──────┘
为什么要三次,而不是两次?关键在第二次和第三次。设想一下只有两次握手的情况:客户端发 SYN,服务器回 SYN+ACK,双方就认为连接建立了。但如果客户端的第一个 SYN 在网络里延迟了,客户端等不及又发了一个 SYN,服务器先收到后一个并建立了连接,后来第一个延迟的 SYN 才到,服务器又建立一条"幽灵连接"——而客户端根本不知道有这条连接存在。
三次握手通过第三次的 ACK 确认,让客户端明确告诉服务器"我确实要建这条连接",避免了这种历史报文造成的错误连接。
数据传输:可靠交付是怎么做到的
连接建立后,TCP 开始传输数据。TCP 的"可靠"体现在几个机制上:
TCP 可靠交付的四大保障
├── 序号与确认(ACK)
│ └── 每个字节都有序号,接收方收到后回 ACK 确认
├── 超时重传
│ └── 发出去的包没收到 ACK,超过时间就重发
├── 流量控制(滑动窗口)
│ └── 接收方告诉发送方"我还能收多少",别发太快
└── 拥塞控制
└── 检测网络拥堵,主动降低发送速度
这里有个很容易踩的坑——粘包。TCP 是字节流协议,没有消息边界。你连续 send 两个包,接收方可能一次 recv 收到合并后的数据,也可能只收到半个。所以在做网络编程时,必须自己定义消息边界(比如在消息头写长度,或者用特殊分隔符)。这也是我在 NDATools 里花了不少时间解决的问题。
四次挥手:优雅地断开
数据传输完了,断开连接需要"四次挥手":
客户端 服务器
│ │
│── FIN (seq=u) ──────────────────→│ 第 1 次:我说完了,我要关了
│ │
│←─ ACK (ack=u+1) ────────────────│ 第 2 次:收到,我知道了
│ │
│←─ FIN (seq=v) ──────────────────│ 第 3 次:我也说完了,我也要关了
│ │
│── ACK (ack=v+1) ────────────────→│ 第 4 次:好,关闭
│ │
└──────────── 连接关闭 ──────────────┘
为什么断开要四次,握手只要三次?因为 TCP 连接是全双工的,两个方向的数据可以独立关闭。挥手时,一方发 FIN 表示"我这边没数据要发了",但另一方可能还有数据没发完。所以关闭是分两个方向各走一遍:客户端关它的发送方向(第 1、2 次),服务器关它的发送方向(第 3、4 次)。而握手时双方都要同时开启发送能力,SYN 和 ACK 可以合并成一条,所以少了一次。
还有一个细节是 TIME_WAIT 状态:主动关闭的一方在发出最后的 ACK 后,会进入 TIME_WAIT 并等待一段时间(通常是 2 个最大报文段生存时间)。这是为了确保最后的 ACK 能到达对方——如果 ACK 丢了,对方会重发 FIN,主动方还能再回应。这段时间里端口不能被复用,这也是为什么服务器重启时偶尔会遇到"Address already in use"。
UDP:无连接的"发了就不管"
和 TCP 不同,UDP 是无连接的。它不需要握手,不需要挥手,发送方直接 sendto 把数据丢出去,至于对方收没收到、顺序对不对,UDP 一概不管。
UDP 通信流程(极简)
├── 发送方:socket() → sendto() → 数据发出
└── 接收方:socket() → bind() → recvfrom() 收到数据
没有握手,没有确认,没有重传
这种"简单粗暴"带来了两个直接的好处:一是延迟低,没有建立连接的开销;二是头部小,UDP 头只有 8 字节,而 TCP 头有 20~60 字节。所以 UDP 特别适合那些"丢几个包也没关系,但延迟必须低"的场景——比如视频直播、语音通话、DNS 查询、游戏实时对战。
代价当然是不可靠。数据可能丢失、可能乱序、可能重复,这些都交给应用层自己去处理。这也就是为什么 QUIC 协议(HTTP/3 的底层)会基于 UDP 重新实现一套可靠机制——既想保留 UDP 的低延迟,又想要 TCP 的可靠,于是干脆在应用层自己造轮子。
TCP vs UDP:一张对比图
对比维度 TCP UDP
─────────────────────────────────────────────────────
连接方式 面向连接(三次握手) 无连接
可靠性 可靠(确认+重传) 不可靠(尽力而为)
头部开销 20~60 字节 仅 8 字节
传输模式 字节流(需处理粘包) 数据报(保留边界)
拥塞控制 有 无
典型场景 HTTP/HTTPS、文件、邮件、SSH DNS、直播、语音、游戏、QUIC
动手看看:交互式 TCP / UDP 实验室
光看文字和示意图,有些状态转换还是不够直观。所以我做了一个交互式学习页面,你可以直接点进去,亲眼看三次握手、数据传输、四次挥手的动画过程,还能调整速度、连发 UDP 数据报看效果:
这个页面把上面讲的每个概念都做成了可视化动画,建议配合本文一起看——先读文字建立概念,再点进去看动画加深印象。
写在最后
搞清楚 TCP 和 UDP 的建立连接过程,对我做网络编程的帮助是巨大的。以前写 socket 代码是照着抄,现在能理解每一步在协议层面发生了什么——为什么 recv 会收到半包、为什么服务器重启会报端口占用、为什么 UDP 不用连接。这些"为什么"搞懂了,Debug 的时候就不慌了。
如果你也在学网络编程,强烈建议自己动手写一个最简单的 TCP 客户端/服务端,再用抓包工具看看真实的握手报文。纸上得来终觉浅,亲眼看到那些 SYN、ACK、FIN 在网络里飞来飞去,比背十遍书都有用。