☰
TCP与UDP区别详解:从三次握手原理到实时音视频协议选型与iperf3打流实测
2026/10/7 10:33:02 网站建设 项目流程

面试官问我“TCP和UDP的区别”时,我通常不会直接背那几条对比,而是反问他:“你的业务能不能接受丢包?”这个问题问完,基本就分出了大半。做网络排查、服务端开发、音视频传输这么多年,我越来越觉得UDP和TCP不是简单的“可靠”和“不可靠”的对立,而是两套完全不同的设计哲学。这篇文章我打算把这两个协议从原理到选型彻底讲透,包括三次握手为什么不差一次、挥手为什么非要四次、UDP为什么在实时场景里反而更香,再配一份我用iperf3给UDP和TCP实际打流的测试记录。适合刚入门网络协议的新人,也适合正在做协议选型、排查网络性能问题的老手。

1. 先把问题问对:传输层协议到底在解决什么问题

很多人一上来就背“TCP是面向连接的、UDP是无连接的”,但连接到底是什么,为什么要“面向”,反而说不清楚。要理解UDP和TCP的差异,得先从传输层在整条网络链路里的位置说起。

1.1 端口号是“分拣规则”的核心

网络层(IP层)负责把数据从一台主机送到另一台主机,它只认IP地址。但一台服务器上同时跑着Web服务、数据库服务、日志服务,数据到了这台服务器的网卡之后,交给谁?这就是传输层要解决的“进程到进程”通信问题。

解决办法就是端口号。IP地址相当于一个小区,端口号相当于楼栋门牌号。TCP报文头里带着源端口和目的端口,UDP报文头里也带着这两个端口。数据包到达目标主机后,内核根据目的端口把数据分发给对应进程。这个过程叫“分用”,反过来多个进程各自从自己的端口发数据,最终复用同一个网卡出去,叫“复用”。

搞明白这一点,你就知道端口号不是一个随意的编号,它是传输层最核心的寻址机制。以前排查过不少“端口不通”的问题,最后发现根本不是防火墙拦包,而是服务没起来导致端口根本没监听,或者监听在127.0.0.1上而外部访问的是内网IP地址。所以只要谈到TCP和UDP,一定绕不开端口和监听行为。

1.2 有状态与无状态:TCP和UDP的第一个分岔

传输层协议的设计分岔点就在“要不要为通信双方维护状态”。

TCP选择了一条极其复杂的路:它在通信之前先建立连接,通信过程中维护序号、确认号、窗口、拥塞状态等一系列信息,通信结束后还要专门拆除连接。每一端的操作系统内核里都维护着一张连接状态表,记录这条连接当前的阶段、序列号、收了多少字节、发了多少字节。

UDP则选择了一条极简的路:它不建立连接,不维护状态,一个报文就是一条独立的消息。发送方把数据扔给IP层就算完事,接收方收到什么算什么。用打电话和寄明信片来类比:TCP是打电话——先拨号接通,说完再挂断,双方能确认彼此在线;UDP是寄明信片——写完丢进邮筒,不保证对方一定收到,更不保证顺序。

这里要注意,无状态并不是UDP的缺陷。恰恰因为无状态,UDP才能做到极低的开销、极低的延迟、以及天然支持广播和多播。后面我会专门展开讲,工程里很多棘手问题反而是靠UDP解决得干净利落的。

2. TCP的三次握手不是仪式,是状态协商

“三次握手”大概是整个TCP协议里被讲烂的概念,但真正理解它为什么是三次的人并不多。面试时我常让人把三次握手的报文画出来,能画对的不少,可一旦追问“为什么两次不行”,很多人就开始含糊了。

2.1 为什么非要是三次

TCP三次握手的报文序列是这样的:

  1. 客户端发送SYN报文,seq为客户端初始序号x,进入SYN_SENT状态;
  2. 服务端收到后,回复SYN+ACK报文,seq为服务端初始序号y,ack为x+1,进入SYN_RCVD状态;
  3. 客户端收到SYN+ACK后,再回复ACK报文,ack为y+1,双方进入ESTABLISHED状态。

核心在于:握手的目的不只是“确认对方在线”,而是要确认双方的收发能力都正常、并且互相同步初始序列号。第一次握手让服务端知道客户端能发;第二次握手让客户端知道服务端能收能发;第三次握手让服务端知道客户端能收。只有三次才能让双方都确认“我能发你也收得到,你能发我也收得到”。

如果只用两次握手,最经典的场景是:客户端第一个SYN报文在网络上滞留了,客户端超时重传了第二个SYN,服务端收到第二个SYN后回了SYN+ACK,连接建立成功。双方通信、关闭。这时,第一个迟到的SYN又到达服务端,服务端把它当作一个全新连接请求,回一个SYN+ACK,然后傻傻地等待客户端回ACK。这个连接永远不会被使用,白白占用资源——这就是半开连接。三次握手时客户端收到服务端对旧SYN的响应,可以通过比对序号判断这不在自己期望的连接里,直接回RST拒绝掉。

2.2 握手背后的两个连接队列

三次握手期间,服务端存在两个队列:SYN队列(半连接队列)和Accept队列(全连接队列)。

客户端SYN到达后,服务端内核先在半连接队列里创建一个条目,状态是SYN_RCVD。服务端回复SYN+ACK之后,客户端回完最后一个ACK,这条连接才能从半连接队列移到全连接队列,等待应用进程调用accept()。

这两个队列的容量在Linux下都有对应的内核参数,半连接队列大小由net.ipv4.tcp_max_syn_backlog等参数控制,全连接队列与应用程序listen时的backlog参数直接相关。生产环境中如果出现“握手反复超时”“客户端连不上但服务端负载很低”这类现象,我经常会先看这两个队列是否溢出。

提示:这里的队列溢出通常是“被动”信号,真正要查的是为什么连接堆积。应用层处理慢、线程池耗尽、accept()不调用,都会导致全连接队列压满。一味调大backlog,往往只是把问题往后推。

2.3 生产环境中常见的握手异常现象

实际排障中,三次握手相关的常见问题主要有几类:

  • SYN重传:客户端发SYN后迟迟收不到SYN+ACK,然后1秒、2秒、4秒地重传。原因通常是防火墙丢包、服务端半连接队列满、或者服务端没有监听该端口。
  • SYN泛洪式攻击:攻击者只发SYN不回ACK,把半连接队列打满。缓解思路是SYN Cookies——不分配完整的连接条目,用加密方式把连接信息编码在SYN+ACK里,直到收到最后的ACK再恢复连接。
  • 握手完成但应用超时:三次握手已经完成,连接进入了全连接队列,但应用层进程迟迟不accept,客户端认为“连接已建立”,实际却等不到任何数据。这类问题用ss -lnt看看Recv-Q就能发现端倪。

这类问题排查时,tcpdump抓包是最好的手段。只要在客户端和服务端同时抓包,对比报文收发,就基本能定位是哪个环节出了问题。

3. TCP的可靠性机制,是一套精心设计的“交换”

TCP之所以被叫作“可靠传输协议”,是因为它用一整套机制换来了确定性的交付。这套机制包括序号与确认、超时重传、快速重传、流量控制、拥塞控制。逐项拆开看,每一项背后都是“以更多报文和更复杂的计算,换取数据的完整有序”。

3.1 序号、确认与重传的三角关系

TCP发送的不是单个报文的“已收到”确认,而是累计确认。每个字节都有一个序号。比方说发送方发了1000字节,接收方全收到了,回一个ack=1001,表示“1001之前的所有字节我都收到了,下一个我要的是1001”。如果中间丢了200个字节,接收方收到了后续的部分乱序数据,它依然会把ack停留在期望的那个序号上,不断重申“我缺的就是这个”。

发送方收到重复ACK时,说明大概率有丢包或乱序。于是重传机制启动:要么等待超时计时器到期走超时重传,要么收到足够数量的重复ACK触发快速重传。超时重传的等待时间并不是固定的,内核会根据历史RTT动态计算。如果RTT本身比较大,这个等待时间也会跟着变大,这也是“高延迟网络下TCP性能上不去”的一个直接原因。

3.2 重复ACK与快速重传:没必要等超时

光靠超时重传有一个问题:等待时间太长,对交互性要求高的应用几乎是灾难。于是TCP设计了快速重传。当发送方连续收到3个重复ACK(即总共4个相同的ACK)时,它就认为某段数据丢失了,不等计时器到期立刻重传。重复ACK机制结合SACK选项,还能让接收方明确告诉发送方“我已经收到了哪些不连续的数据”,避免发送方把明明已经收到的数据再重传一遍。

我在排查长肥网络(高带宽高延迟)时,特别关注抓包里重复ACK的数量和分布。如果重复ACK总是成批出现,往往不是网络真的丢了包,而是某条路径上有短暂的拥塞,触发交换机或路由器丢弃了部分数据,此时调整拥塞窗口、或者优化应用层的发送节奏,通常比盲目扩容带宽更有效。

3.3 流量控制:接收方也有话语权

可靠传输不能只靠发送方“一股脑发完”。如果接收方处理不过来,缓冲区被写满,再多的包也会被丢弃。TCP流量控制通过接收窗口字段实现:接收方在每个ACK报文的window字段里通告自己的剩余接收缓冲区大小,发送方始终保证在途未确认的字节数不超过这个窗口。

在生活中理解:你往朋友家水管里灌水,朋友在水管口立了一块板子,上面写着“我厨房水槽现剩300升容量”。发送方的发送窗口就是跟随这个数字实时变的。接收缓冲区越忙,通告窗口越小;一旦窗口通告为0,发送方会停下来,并周期性发送窗口探测报文询问“现在能收了吗”。

3.4 拥塞控制:别把公共道路占满

流量控制照顾的是接收方的处理能力,拥塞控制照顾的是整条链路的承载能力。内核维护着一个拥塞窗口cwnd,实际发送量是min(发送窗口,拥塞窗口)。

拥塞控制大致分这么几段:

  • 慢启动:连接建立初期cwnd很小,每个RTT翻倍,快速探测链路的可用带宽;
  • 拥塞避免:当cwnd超过ssthresh阈值后,每个RTT线性增加,试探性地靠近链路极限;
  • 超时或丢包时:cwnd大幅回落,进入快速恢复阶段,而不是从零开始。

Linux默认的拥塞控制算法已经从老式Reno换成了CUBIC,在高带宽长距离链路上表现更好。网上有关“TCP单线程跑不满带宽”的吐槽,很大一部分就是拥塞控制算法对高延迟链路不友好导致的,合理调整或换用BBR等算法往往能立竿见影。

4. UDP的无状态设计:不是缺点,是另一种工程思路

现在可以回到UDP正题了。很多人讲UDP只会说“不可靠”“会丢包”,仿佛它是TCP的低配版。实际恰恰相反,UDP是一个极其精巧的取舍结果,它在大量对延迟敏感、对状态复杂性不买账的场景里,是比TCP更合适的选择。

4.1 8字节的报文头与零状态机

UDP报文头只有8个字节:源端口、目的端口、长度、校验和。对比TCP至少20字节的报文头,UDP把每报文的开销压到最低。更重要的是UDP没有连接状态机,没有SYN、FIN、ACK,没有序号,没有滑动窗口。每个数据报都是独立的消息,内核不需要为它维护任何长期状态。

IPv4下UDP校验和是可选字段,IPv6下则是必选字段。现代系统基本都会开启校验和,毕竟一个校验位能省掉大量因传输损坏导致的静默错误。UDP的接收粒度是“整个报文”,而TCP是“字节流”。应用从UDP socket读出的数据,就是当初发送方write进去的那一整块数据,边界清晰。

4.2 为什么实时场景更愿意选择UDP

TCP引入的可靠性和流量控制,意味着它内置了延迟的“不确定性”。一个包丢了,要等重传,之后的数据就算已经到达了接收方,也可能被卡在缓冲区里,要等丢掉的包补上才能交给应用。这就是TCP的队头阻塞问题。

视频通话、在线游戏、语音聊天这类场景,用户能接受偶尔花屏、偶尔瞬间卡顿,但无法接受“为了等一个迟到的包把整路声音憋住”。所以这些系统要么直接走UDP,在应用层只对关键帧做重传;要么走基于UDP的QUIC协议,用应用层实现的可靠传输替代内核里写死的TCP逻辑。

4.3 在UDP之上重建可靠性的需求

UDP的无状态并不代表UDP之上不能做可靠传输。QUIC就是一个典型的例子:它跑在UDP之上,在用户态实现连接管理、可靠传输、拥塞控制。既然TCP已经做了这些事,为什么还要在UDP上另起炉灶?

关键原因在于控制权。TCP的可靠性和拥塞控制逻辑放在操作系统内核里,你想换一个更合适的拥塞控制算法、想取消队头阻塞、想快速迁移连接(比如手机从Wi-Fi切到4G),改动内核几乎不现实。把协议搬到用户态,迭代速度、可定制性、可观测性全都上来了。这正是“看似不可靠的UDP,反而承载起了下一代传输协议”的根本逻辑。

5. 挥手四次与TIME_WAIT:连接关闭才是坑最多的地方

TCP是面向连接的,连接有建立就有关闭。连接建立的坑相对少,连接关闭的坑我在生产环境里踩过太多回了。这里必须把四次挥手和TIME_WAIT讲透。

5.1 四次挥手的完整链条

TCP是全双工的,每个方向的关闭都是独立的。所以完整的关闭需要四次报文:

  1. 主动关闭方发送FIN,进入FIN_WAIT_1;
  2. 被动关闭方回复ACK,进入CLOSE_WAIT,主动方进入FIN_WAIT_2;
  3. 被动关闭方应用层调用close后,发送FIN,进入LAST_ACK;
  4. 主动关闭方回复ACK,进入TIME_WAIT,被动方进入CLOSE。

为什么主动和被动不能同时各发一个FIN就完事?因为被动方收到FIN后,可能还有数据没发完,它必须先把剩余数据发完再关闭。所以关连接的第二次和第三次报文是分开的:先确认收到对方的FIN,等自己的数据处理完再单独发FIN。

5.2 TIME_WAIT存在的两个理由

主动关闭方在最后一次确认ACK发完后,不会立刻关闭,而要进入TIME_WAIT状态,等待2MSL(最大报文段生存时间的两倍)时间。这是很多运维和开发最不理解的设计。

两个理由其实都很有说服力。第一,最后一个ACK可能丢失,被动方会重发FIN,主动方需要留在那个状态以便重发ACK;如果主动方直接关闭,被动方会一直等在LAST_ACK状态。第二,确保本连接的所有报文在网络中彻底消失,避免“同一个四元组的旧连接报文”被新连接错误接收。

TIME_WAIT是TCP可靠性的最后一道防线,不能一删了之。

5.3 高并发短连接下的调优边界

大量短连接场景(比如常见的HTTP短连接、监控采集),TIME_WAIT积累会非常可观。在网上搜“TIME_WAIT太多怎么办”,能搜出不少“秘籍”,最典型的是开启tcp_tw_recycle。这里要明确说一句:tcp_tw_recycle是一个历史遗留参数,它在NAT环境下会导致大量连接异常重置,Linux内核从4.12版本开始已经把它移除了,任何还在用它的方案都该淘汰。

现实中更稳妥的手段是:应用层复用长连接、调整服务端keep-alive策略、对服务端socket设置SO_REUSEADDR。TIME_WAIT本身并不致命,真正致命的是因为TIME_WAIT连接数过多而耗尽本地端口。把短连接改成连接池,比在参数上钻牛角尖有效得多。

注意:如果你想看当前系统有哪些TIME_WAIT / CLOSE_WAIT连接,直接执行ss -tan state time-wait或ss -tan state close-wait,一秒钟就能统计出来。CLOSE_WAIT大量堆积通常意味着应用层有连接没有调用close,属于代码层面的泄漏问题。

6. 实测对比:用iperf3做TCP与UDP打流

理论讲再多,不如跑一把数据。我自己做网络诊断或者验证线路质量时,几乎离不开iperf3,它能同时测TCP吞吐、UDP带宽/丢包/抖动,是排查网络问题的一把快刀。

6.1 UDP打流前必须明白的一件事

很多人第一次用iperf3跑UDP测试时,习惯性地只跑iperf3 -c <目标IP>,结果看到带宽数据“异常好看”,实际上是把整条链路打满了。

UDP没有拥塞控制机制,只要应用层一直发,网络就会一直传,直到链路或者对端负载顶不住。iperf3在UDP模式下,默认会以1Mbps的速率尽力发送,这显然不符合“压测”的目标。所以我一般都会显式指定目标带宽:

iperf3 -c <服务端IP> -u -b 1000M -t 60

这条命令的含义是以1000Mbps即1Gbps的目标速率发送UDP数据60秒。只有指定了目标带宽,得到的丢包率、抖动值才有参考意义。

6.2 TCP基准测试

先在服务端启动监听:

iperf3 -s

客户端测TCP吞吐:

iperf3 -c <服务端IP> -t 60 -P 4

-P 4表示开4个并发流。TCP模式下iperf3在结束后会输出每秒的带宽数据,以及整个测试过程中的重传数量。重传率是评估链路质量的重要指标:如果在带宽未打满的情况下重传依然很多,说明链路中可能存在丢包、错包或缓冲区不足。

6.3 UDP打流测试与指标解读

服务端启动方式和TCP一致,客户端稍作区分:

iperf3 -c <服务端IP> -u -b 1000M -t 60 -l 1470

-l 1470把UDP数据报负载控制在1470字节,这是基于以太网MTU 1500算出来的合理值:1500减去20字节IP头、再减去8字节UDP头,正好1470字节。如果载荷设置太大,UDP报文会被IP层分片,分片丢一个就丢整包,测出来的丢包率会异常失真,这不是链路真实水平。

UDP测试的核心输出包括三部分:

  • 带宽:实际接收速率是否达到目标;
  • 抖动(Jitter):相邻报文到达时间间隔的变化,音视频业务对抖动极其敏感;
  • 丢包率(Lost/Total Datagrams):接收端实际丢了多少包,方向是服务端到客户端。

我曾经用这个方法帮一个直播团队排查问题:TCP测试带宽跑到900M没问题,UDP目标1Gbps时丢包率只有0.02%,但现场直播偶尔卡顿。后来把测试时长拉到300秒,丢包率依然很低,抖动却在某一分钟突然飙到几十毫秒。再往下挖,才发现是在那几秒内交换机上另一个端口有大量突发流量,导致缓冲队列抖动。

6.4 从测速看网络质量

有人问,既然有测速网站为什么还要自己打流。原因很简单,测速网站只能测“这个站点到你之间”的路径,而自己用iperf3打流是任取两端,精确控制带宽、时长、数据包大小,能覆盖包括服务器之间、专线之间、客户端到机房内部的任何路径。再配合tcpdump或wireshark抓包分析,基本能回答“我的网络是带宽不够,还是时延太高,还是丢包严重”这个最经典的问题。

7. 我最后做选型的五个判断问题

把UDP和TCP的原理讲完,最终还得落回一个很实际的问题:我的场景到底该用哪个?每次做技术选型,我心里都有一个固定的决策清单:

  1. 数据能容忍延迟抖动吗?视频、语音、游戏操作这类要实时反馈的,优先UDP;如果面向公网传输又想要可靠,直接考虑QUIC。
  2. 数据能容忍丢失和乱序吗?完全不能丢的,用TCP最稳;能接受少量丢失、靠应用层补偿的,UDP+自定义重传更高效。很多实时传输系统是“关键帧走可靠重传,非关键帧直接丢弃”,这种弹性只有UDP给得了。
  3. 连接是否短命且高频?大量高频短连接,TCP的握手和挥手开销是真实成本。要么用连接池、要么评估UDP,否则请求还没开始就要先付一个RTT的连接成本。
  4. 网络环境控制权有多大?公网跨运营商、穿越NAT和防火墙时,TCP的“面向连接”特征通常能获得更友好的中间设备支持;UDP在某些网络环境里丢包率会高得离谱,必须先做打流验证,不要拍脑袋直接上。
  5. 是否需要自己掌握传输细节?需要自定义拥塞控制、需要0-RTT建立连接、需要连接迁移,就选UDP并封装自己的可靠传输层;这些需求都不强烈,直接用TCP,让内核替你干活,最省心。

我把TCP和UDP的关键特性放到一张表里,方便对照:

对比维度TCPUDP
连接状态面向连接,维护状态机无连接,无状态
报文头开销至少20字节8字节
可靠性确认、重传、顺序保证尽力而为,不保证
流量控制有,滑动窗口无
拥塞控制有,内核算法干预无,应用自控
延迟特性有队头阻塞,延迟不确定低延迟,延迟可预期
典型场景网页、文件、数据库、交易实时音视频、游戏、DNS、广播
传输单位字节流数据报,边界清晰
中间设备友好度较高,NAT/防火墙适配充分需要自行验证穿越性

这张表不是要分出谁好谁坏,而是让你带着具体需求来找答案。网络协议本质是约束下的工程选择,没有万能的协议,只有适不适合你的业务场景。

最后再分享一个个人习惯:我现在排查任何网络质量问题,都不直接改应用代码,而是先用tcpdump抓包观察握手和重传情况,再用iperf3分别跑一遍TCP和UDP打流,把丢包、抖动、重传、带宽四个指标拿到手。这套流程走完,问题基本能定位到链路、中间设备还是应用层。协议原理不是面试时的背诵材料,它应该成为你排障时手里最有用的那几张牌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询