MMyBlog
← 返回博客
网络编程
10 分钟·3,715 ·18 次阅读

图解 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 实验室

这个页面把上面讲的每个概念都做成了可视化动画,建议配合本文一起看——先读文字建立概念,再点进去看动画加深印象。

写在最后

搞清楚 TCP 和 UDP 的建立连接过程,对我做网络编程的帮助是巨大的。以前写 socket 代码是照着抄,现在能理解每一步在协议层面发生了什么——为什么 recv 会收到半包、为什么服务器重启会报端口占用、为什么 UDP 不用连接。这些"为什么"搞懂了,Debug 的时候就不慌了。

如果你也在学网络编程,强烈建议自己动手写一个最简单的 TCP 客户端/服务端,再用抓包工具看看真实的握手报文。纸上得来终觉浅,亲眼看到那些 SYN、ACK、FIN 在网络里飞来飞去,比背十遍书都有用。

文章链接: