写单片机的同事问我,数据上报用 UDP 还是 TCP;写上位机的同事问我,TCP 连上十分钟就断是什么原因;做测试的同事指着 iperf3 的输出问我,这个吞吐量到底是怎么算出来的。我发现,只要沾到网络通信,TCP 和 UDP 永远是绕不开的两个名字。不管你做嵌入式、上位机、Web 后端还是网络运维,最终都会遇到一个问题:这条数据,到底走 TCP 还是走 UDP?
这篇文章不是教科书式的协议讲解,而是我这些年实际写网络程序、调网络问题时的经验总结。我会把 UDP 和 TCP 的区别、三次握手四次挥手为什么存在、实际开发中怎么选型、怎么用代码实现、怎么用 iperf3 和抓包工具验证,以及那些你在百度上经常搜到却说不清楚的报错,一次讲明白。适合刚接触网络编程的新手,也适合被各种奇奇怪怪网络问题折腾过的老手。
1. 先搞清楚UDP和TCP在协议栈里的位置
1.1 一张图看懂数据从应用到网线要经过什么
每台联网的设备,只要走标准的 TCP/IP 协议栈,数据从你写的程序里发出去,会依次经过应用层、传输层、网络层、链路层。你写的 HTTP、Modbus TCP、FTP 这些是应用层协议;而 UDP 和 TCP 是传输层协议,它们俩是“平级”的,都在应用层下面,负责把你应用层的数据包装成报文,然后交给 IP 层去路由转发。
如果把网络通信比作寄快递,应用层决定包裹里装什么(比如你要传的传感器数据),传输层决定用哪种运输方式,网络层负责规划路线(IP 地址),链路层负责实际把包裹搬上车(MAC 地址、网卡驱动)。UDP 和 TCP 就是那两种截然不同的运输方式:一种是你扔到快递柜里就不管的平邮,一种是要求签收、丢件必赔的保价专线。
很多初学者会混淆“TCP/IP”和“TCP”这两个词。TCP/IP 其实是一整组协议族的统称,包括 IP、TCP、UDP、ICMP、ARP 等等;而 TCP 只是其中的一个成员。所以当你看到“支持 TCP/IP 协议”这种描述时,它的意思往往是“支持标准的网络通信”,不一定特指 TCP。这也是为什么很多硬件网卡宣称支持“TCP/IP 卸载”,其实说的是整个协议栈的处理能力,不是单指 TCP 一种协议。
1.2 端口号和四元组:通信双方怎么找到对方
不管是 TCP 还是 UDP,应用层的数据最终都要靠“IP 地址 + 端口号”来寻址。IP 地址告诉你数据要去哪台机器,端口号告诉你这台机器上的哪个进程在接收。一台服务器上同时跑着 SSH、Web 和数据库服务,靠的就是 22、80、3306 这些端口号把它们区分开。
这里有个细节容易被忽略:TCP 和 UDP 的端口是独立的,同一个端口号可以同时被一个 TCP 服务和一个 UDP 服务使用,互不干扰。比如 DNS 服务既监听 53 端口的 UDP,也监听 53 端口的 TCP,两条路径同时存在。我在排查端口冲突时经常遇到新手问“我明明 8080 端口被占了,为什么 netstat 里看到两个程序都用 8080”——你去看一下协议列,大概率一个是 TCP 一个是 UDP,那就不叫冲突,只是你被吓到了而已。
TCP 通信还有一个重要概念叫“四元组”:源 IP、源端口、目标 IP、目标端口。这四样合在一起才能唯一定位一条 TCP 连接。很多并发服务器的设计都依赖这个特性,同一个服务端口,可以同时跟成千上万个客户端建立连接,因为每条连接的客户端 IP 和端口不同,四元组就不会重复。UDP 没有连接的概念,它只有“信封”上的源地址和目标地址,发完就完事,不维护任何双方状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. TCP的核心机制:三次握手、四次挥手与可靠性设计
2.1 三次握手:为什么不是两次或四次
TCP 是面向连接的可靠传输协议,连接建立的标志就是那经典的三次握手。客户端先发一个 SYN 包,表示“我要和你建立连接”;服务器回一个 SYN+ACK,表示“我收到了,我也准备好了”;客户端再回一个 ACK,表示“我确认你也准备好了”。连接正式建立。
这三次里最关键的是第二次,它同时干了两件事:确认对方的 SYN,同时发起自己的 SYN。如果你觉得“确认”和“发起”可以拆成两步,那就会变成四次握手,但 TCP 觉得没必要,合并成一步节省一次往返,所以本质上三次握手是最优状态。
那为什么不是两次?假设客户端发的第一个 SYN 在网络里卡了很久,客户端超时重传了一个新的 SYN,然后跟服务器正常建立了连接,用一阵子后关了。结果这时第一个 SYN 才慢吞吞到达服务器,服务器一看有人要建连,就回了 SYN+ACK。如果只有两次握手,服务器会以为连接已经建立成功,开始分配资源等待数据,然后一直等不到,白白浪费资源。有了第三次握手,客户端发现这个连接我已经不需要了,直接回一个 RST 告诉服务器“别等了我不要”,服务器就能及时释放分配的 PCB(进程控制块)资源。所以三次握手不只是“礼貌”,更是为了防止过期连接请求对服务器造成资源浪费。
2.2 四次挥手:为什么断开要比建立多一次
断开连接的四次挥手,很多背过面试题的人都能默写:A 发 FIN,B 回 ACK,B 再发 FIN,A 回 ACK。但真正明白为什么需要四次的没几个。
原因在于 TCP 是全双工通信,数据可以双向独立传输。A 说“我不发了”,只是 A→B 这个方向的数据结束了,但 B→A 这个方向 B 可能还有数据没发完。所以 B 先回一个 ACK,告诉 A“我知道你不发了”,然后继续把剩余数据发完,再发自己的 FIN,告诉 A“我也不发了”,最后 A 回最后一个 ACK。整个过程合起来才是彻底断开。
注意一个细节:B 回 ACK 和 B 发 FIN 之间,是可以有间隔的。这个间隔的长短取决于 B 还有多少数据要发。实际写代码时,如果你在服务器端调用 close() 之前没有把响应数据 flush 干净,就可能出现客户端收到半截数据的情况。这也是我调试 TCP 程序时最常遇到的问题之一。
还要提一下 TIME_WAIT 状态。主动关闭连接的一方(也就是第一个发 FIN 的那一方),在发出最后一个 ACK 后会进入 TIME_WAIT 状态,默认等 2MSL(报文最大生存时间的两倍,常见值 60 秒)。很多人不理解为什么要等这么久,其实是为了防止“最后一个 ACK 丢失,对方重发 FIN 时,我这边需要能再回应”。同时,也让网络上还在迷路的旧数据包彻底消失,避免影响新建的连接。写高并发服务器的都知道,如果服务器主动断开大量短连接,会出现一堆 TIME_WAIT 套接字,占用端口资源,这也是“端口不够用”的经典场景。
