搞网络调试这些年,UDP和TCP是我见过被误解最多的两个协议。很多刚入行的朋友一听到“TCP可靠、UDP不可靠”,就直接把UDP打入冷宫;而另一批人为了低延迟,又什么都想往UDP上塞。实际上,这两个协议没有绝对的好坏,关键是你得知道它们各自在底层做了什么、代价是什么。今天我不打算复述教科书,而是结合我实际调试WSL2与Windows的UDP通信、iperf3打流、PLC的Modbus TCP连接等场景,把这两个协议掰开揉碎讲一遍,顺便把那些高频踩坑点也一并整理出来。文章适合刚接触网络编程的开发者,也适合被TCP/UDP疑难杂症折磨的运维和工控工程师。
1. 从三次握手说起:TCP与UDP的本质差异
1.1 三次握手到底在“握”什么?为什么非要三次?
很多人背过TCP三次握手:客户端发SYN,服务端回SYN+ACK,客户端再回ACK。但问一句“为什么非得三次”,不少人就卡住了。我习惯用一个很生活化的类比:你给同事发微信“在吗”,同事回“在的,你是哪位”,你再回“我是老张”。这三句话看起来啰嗦,但双方都确认了一件事——我发的消息你能收到,你发的我也能收到。TCP的三次握手就是干这个的:客户端确认自己的发送和接收能力没问题,服务端也确认自己的发送和接收能力没问题,双方各自维护一个起始序号,后续数据才能按序重组。
第三次ACK还有一个隐藏作用:防止历史失效请求占用服务端资源。如果客户端第一次发的SYN因为网络拥堵迟到了,服务端收到后回了一个SYN+ACK,此时客户端发现这个连接已经不是自己想要的,就回一个RST而不是ACK,服务端就知道这个连接可以彻底丢弃。少了这第三次,服务端只能傻等,连接资源很容易被耗尽。这就是为什么SYN Flood攻击能成为经典攻击方式——它只发SYN不响应ACK,让服务端一直挂着半连接。
四次挥手也是同理。TCP是全双工的,双向数据通道要分别关闭。一端说“我没有数据要发了”(FIN),另一端说“我收到了”,但这一端可能还有数据要回,所以不能马上关,只能先回ACK,等自己把数据发完再回一个FIN。收到第二个FIN后再回ACK,然后进入TIME_WAIT状态等一会儿。TIME_WAIT很多人觉得烦,但它能保证最后一次ACK不丢,同时让旧连接的延迟数据包在网络中自然消散,避免污染新连接。
1.2 UDP的“无连接”不是缺陷,而是特性
UDP就简单粗暴得多:把数据打包成数据报,写上源端口和目的端口,发出去完事。它不建立连接,不确认对方是否存在,不保证数据到达,不保证顺序。听起来很“菜”,但正因为没有这些机制,它才能做到极低的额外开销和极低的延迟。
我经常打个比方:TCP像快递员上门收件,要打电话确认、签字、可能还要等对方回执;UDP像从楼上往楼下扔纸飞机,扔出去就不管了。纸飞机可能吹歪,可能掉地上,但如果你楼下站着一堆人,每人接住一张纸就能立即做动作,那扔纸飞机反而是最快的分发方式。
关键点是:UDP的“不可靠”不等于“不能用”。很多新协议,比如QUIC,就是在UDP之上实现了一套自己的可靠性机制。说白了,TCP的可靠是协议层做好的,UDP的可靠需要应用层自己决定做不做、怎么做。如果你只是需要局域网内广播一条设备状态,UDP一发就完事;如果你要传文件,则可以自己在UDP上做分片、超时、重传。灵活性和可控性,恰恰是UDP的核心优势。
1.3 一张表看懂TCP和UDP的关键参数
我把常用对比整理成一张表,后续聊选型和排查问题都会用得到:
| 对比项 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接,需三次握手 | 无连接,直接发包 |
| 数据边界 | 字节流,无消息边界 | 数据报,天然保留消息边界 |
| 头部大小 | 20~60字节 | 8字节 |
| 可靠性 | 有确认、重传、去重 | 没有保证 |
| 顺序性 | 按序交付 | 可能乱序 |
| 流量控制 | 有滑动窗口 | 无 |
| 拥塞控制 | 有,拥塞时自动降速 | 无,发送快就会打满链路 |
| 传输效率 | 相对低,握手和确认占开销 | 高,适合实时数据 |
| 典型场景 | Web、文件传输、邮件、工业协议 | DNS、音视频、实时控制、广播发现 |
这张表能解释不少日常问题。比如“为什么DNS用UDP也能可靠?”因为DNS查询往往就一个包,丢了大不了重新查一次,TCP握手反而更慢。“为什么视频直播很少用TCP?”因为TCP一旦丢包会重传,重传的旧画面到了用户端反而造成卡顿和延迟,UDP直接丢旧帧、显示新帧更流畅。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选型判断题:什么时候该用TCP,什么时候该用UDP
2.1 我踩过的选型坑:实时控制别迷信TCP
有段时间我帮一个自动化项目做设备联动,A设备每隔几十毫秒要给B设备发一次位置信息。一开始我图省事直接用TCP,结果调试时发现数据延迟忽高忽低,偶尔还出现几百毫秒的抖动。后来抓包才意识到,TCP一遇到网络拥塞就自动重传,重传的数据虽然最终能到,但到达时间完全不可控。对运动控制这种场景,老数据晚到比丢包更可怕,控制算法要的是“当前时刻的位置”,不是“半秒钟前的位置”。
后来我改成UDP,自己维护最新的位置状态,接受少量丢包,效果反而好得多。但代价是:需要自己在应用层加序号,判断包是不是乱序;需要自己处理超时,长时
