用UDP就一定比TCP快吗?

摘要:从一次"改成UDP后反而更慢了"的性能优化翻车出发,深度剖析UDP和TCP的性能差异。通过丢包重传、拥塞控制、以及应用层实现可靠性的代价分析,揭秘为什么UDP不一定比TCP快、什么场景UDP确实快、以及QUIC协议如何兼顾UDP的速度和TCP的可靠性。配合抓包图展示协议开销,给出视频直播、游戏、文件传输等场景的协议选型建议。


💥 翻车现场

周三下午,哈吉米在优化实时消息推送功能。

哈吉米(看文章):"UDP没有握手、没有重传,比TCP快多了!我把消息推送改成UDP试试!"

原来的TCP实现

// TCP消息推送
Socket socket = new Socket("server", 8080);
socket.getOutputStream().write(message.getBytes());

改成UDP

// UDP消息推送
DatagramSocket socket = new DatagramSocket();
byte[] data = message.getBytes();
DatagramPacket packet = new DatagramPacket(
    data, 
    data.length, 
    InetAddress.getByName("server"), 
    8080
);
socket.send(packet);

压测结果

TCP实现:
- QPS: 5000
- 平均响应时间: 10ms
- 丢包率: 0%

UDP实现(没有可靠性保证):
- QPS: 8000
- 平均响应时间: 5ms
- 丢包率: 15%  ← 丢包严重

UDP实现(加了应用层重传):
- QPS: 3000  ← 反而变慢了
- 平均响应时间: 25ms
- 丢包率: 0%

哈吉米:"卧槽,UDP加了可靠性保证后,反而比TCP慢?"

南北绿豆和阿西噶阿西来了。

南北绿豆:"UDP不是银弹!如果需要可靠性,自己实现比TCP的内核实现更慢。"
哈吉米:"???"
阿西噶阿西:"来,我给你讲讲UDP和TCP的性能对比。"


🤔 UDP快在哪?TCP慢在哪?

TCP的开销

阿西噶阿西在白板上列出TCP的开销。

TCP的开销:

1. 连接建立(三次握手)
   - 3次网络往返
   - 耗时:3 × RTT(往返时间)
   - 示例:RTT=50ms,握手耗时150ms

2. 连接释放(四次挥手)
   - 4次网络往返
   - 耗时:4 × RTT
   - 示例:RTT=50ms,挥手耗时200ms

3. 数据传输(可靠性保证)
   - 每个包都要等ACK
   - 顺序保证(乱序要重排)
   - 流量控制(滑动窗口)
   - 拥塞控制(慢启动、拥塞避免)

4. 协议头开销
   - TCP头:20-60字节
   - IP头:20字节
   - 总计:40-80字节

总开销(发送1KB数据):
握手150ms + 传输10ms + 挥手200ms = 360ms

UDP的开销

UDP的开销:

1. 连接建立
   - 无需握手(0ms)

2. 连接释放
   - 无需挥手(0ms)

3. 数据传输
   - 直接发送
   - 不等ACK
   - 无流量控制
   - 无拥塞控制

4. 协议头开销
   - UDP头:8字节
   - IP头:20字节
   - 总计:28字节

总开销(发送1KB数据):
传输5ms = 5ms

对比

发送1KB数据:
TCP:360ms(包含握手挥手)
UDP:5ms

性能差距:72倍

但:
如果是长连接(连接复用):
TCP:10ms(只有传输,无握手挥手)
UDP:5ms

性能差距:2倍

南北绿豆:"看到了吗?短连接场景UDP确实快,但长连接场景差距不大。"


🤔 如果UDP要可靠传输呢?

应用层实现可靠性

阿西噶阿西:"如果UDP要可靠传输,必须自己实现TCP的那些机制。"

/**
 * 可靠UDP(应用层实现)
 */
public class ReliableUdp {
    
    private DatagramSocket socket;
    private Map<Long, PacketInfo> sentPackets = new ConcurrentHashMap<>();
    
    /**
     * 发送数据(带重传)
     */
    public void send(String message) throws Exception {
        long seq = sequenceNumber.getAndIncrement();
        
        // 1. 封装数据包(序列号 + 数据)
        UdpPacket packet = new UdpPacket(seq, message);
        byte[] data = serialize(packet);
        
        // 2. 发送UDP包
        DatagramPacket udpPacket = new DatagramPacket(
            data, data.length, serverAddress, serverPort
        );
        socket.send(udpPacket);
        
        // 3. 记录已发送的包(用于重传)
        PacketInfo info = new PacketInfo(packet, System.currentTimeMillis());
        sentPackets.put(seq, info);
        
        // 4. 启动重传定时器
        scheduleRetransmit(seq);
    }
    
    /**
     * 重传定时器
     */
    private void scheduleRetransmit(long seq) {
        scheduledExecutor.schedule(() -> {
            PacketInfo info = sentPackets.get(seq);
            
            if (info != null && !info.isAcked()) {
                // 未收到ACK,重传
                info.retryCount++;
                
                if (info.retryCount < 15) {
                    // 重传
                    try {
                        socket.send(info.getUdpPacket());
                        // 继续定时器(指数退避)
                        scheduleRetransmit(seq);
                    } catch (IOException e) {
                        log.error("重传失败", e);
                    }
                } else {
                    // 重传15次失败,放弃
                    log.error("seq={}发送失败,超过重传次数", seq);
                    sentPackets.remove(seq);
                }
            }
        }, 200, TimeUnit.MILLISECONDS);  // 200ms后重传
    }
    
    /**
     * 接收ACK
     */
    public void receiveAck(long seq) {
        PacketInfo info = sentPackets.get(seq);
        if (info != null) {
            info.setAcked(true);
            sentPackets.remove(seq);  // 收到ACK,移除
        }
    }
}

代码行数对比

实现代码量复杂度
TCP1行(socket.send())简单(内核实现)
可靠UDP200行(应用层实现)复杂(自己实现所有机制)

性能对比

TCP(内核实现):
- 高度优化
- C语言编写
- 内核态执行(快)

可靠UDP(应用层实现):
- Java编写(相对慢)
- 用户态执行
- 上下文切换开销

结果:
可靠UDP比TCP慢30-50%

南北绿豆:"所以需要可靠性时,TCP比自己实现的可靠UDP快得多!"


🎯 什么时候UDP确实比TCP快?

场景1:实时音视频(容忍丢包)

视频直播:
- 帧率:30fps(每秒30帧)
- 丢1-2帧:用户感觉不到(人眼容忍)
- 重传1帧:可能已经过时(新帧已到)

策略:
- 用UDP直接发送
- 丢包不重传(宁可花屏,不要卡顿)

优势:
- 延迟低(不等ACK)
- 实时性好

代码示例

// 视频帧发送(UDP)
public void sendVideoFrame(byte[] frame) {
    DatagramPacket packet = new DatagramPacket(
        frame, frame.length, clientAddress, clientPort
    );
    
    socket.send(packet);  // 发送即忘记(Fire and Forget)
    // 不等ACK,不重传
}

场景2:游戏(低延迟优先)

游戏场景:
- 玩家位置每50ms更新一次
- 丢1次位置:下次会更新(影响不大)
- 重传1次位置:可能已经过时

策略:
- 用UDP发送位置
- 丢包不重传
- 下次更新覆盖

优势:
- 延迟低(10-20ms)
- 体验流畅

场景3:DNS查询(数据量小)

DNS查询:
- 请求:小(< 512字节)
- 响应:小(< 512字节)
- 一次性查询(不需要连接)

用UDP:
发送1个包 → 接收1个包 → 完成

用TCP:
握手3个包 → 发送1个包 → 接收1个包 → 挥手4个包
总共:9个包

UDP优势:
包数量少,延迟低

场景4:广播/组播

场景:
向局域网所有设备发送数据(如设备发现)

TCP:
不支持广播/组播

UDP:
支持广播/组播
→ 1个包发给所有设备

🎯 TCP vs UDP性能对比

短连接场景

发送100次小消息(每次1KB):

TCP:
- 握手:100次 × 150ms = 15秒
- 传输:100次 × 10ms = 1秒
- 挥手:100次 × 200ms = 20秒
- 总计:36秒

UDP:
- 握手:0
- 传输:100次 × 5ms = 0.5秒
- 挥手:0
- 总计:0.5秒

性能差距:72倍

结论:短连接场景,UDP确实快

长连接场景

长连接,发送100次小消息(连接复用):

TCP:
- 握手:1次 × 150ms = 0.15秒
- 传输:100次 × 10ms = 1秒
- 挥手:1次 × 200ms = 0.2秒
- 总计:1.35秒

UDP:
- 传输:100次 × 5ms = 0.5秒
- 总计:0.5秒

性能差距:2.7倍

结论:长连接场景,差距不大

需要可靠性场景

发送100次消息,要求100%送达:

TCP:
- 总计:1.35秒(内核实现,优化好)

UDP(应用层可靠性):
- 传输:100次 × 5ms = 0.5秒
- 重传:假设丢包率5%,5次重传 × 10ms = 0.05秒
- 应用层ACK开销:100次 × 2ms = 0.2秒
- 序列号判断开销:100次 × 1ms = 0.1秒
- 总计:0.85秒

看起来UDP快?

但:
- 代码复杂度:UDP需要200行,TCP需要1行
- 维护成本:UDP要自己处理丢包、乱序、重传
- 稳定性:TCP经过40年优化,UDP自己实现容易出bug

结论:需要可靠性时,TCP更好

🎯 QUIC:UDP的速度 + TCP的可靠性

QUIC协议

南北绿豆:"有没有兼顾UDP速度和TCP可靠性的方案?"

阿西噶阿西:"有!Google的QUIC协议!"

QUIC(Quick UDP Internet Connections):
- 基于UDP
- 在用户态实现可靠性(不在内核)
- HTTP/3的底层协议

特点:
1. 0-RTT连接建立(首次1-RTT,后续0-RTT)
2. 多路复用(一个连接多个流)
3. 改进的拥塞控制
4. 连接迁移(IP变化,连接不断)

QUIC vs TCP性能

握手时间

TCP + TLS 1.2:
1. TCP三次握手(1-RTT)
2. TLS握手(2-RTT)
总计:3-RTT

QUIC:
首次:1-RTT(握手 + 加密)
后续:0-RTT(携带上次的密钥)

性能提升:3倍

QUIC的应用

应用:
- HTTP/3(基于QUIC)
- Google服务(YouTube、Gmail)
- 微信(Mars协议,类似QUIC)

优势:
- 快(0-RTT)
- 可靠(应用层实现)
- 不受内核限制(用户态,快速迭代)

🎓 面试标准答案

题目:用UDP就一定比TCP快吗?

答案

不一定!

分场景讨论

1. 短连接 + 不需要可靠性

  • UDP确实快(无握手挥手)
  • 适用:DNS查询、广播

2. 长连接

  • TCP握手摊销到多次传输
  • 差距不大(UDP快2-3倍)
  • 适用:大部分场景用TCP即可

3. 需要可靠性

  • UDP要自己实现可靠性(应用层)
  • 代码复杂,性能差
  • TCP更快(内核优化好)
  • 适用:文件传输、数据同步用TCP

4. 实时性 > 可靠性

  • UDP确实快
  • 适用:视频直播、游戏、VoIP

性能对比

场景TCPUDP(无可靠性)UDP(有可靠性)
短连接36秒0.5秒3秒
长连接1.35秒0.5秒0.85秒
丢包率0%5-15%0%
代码复杂度简单简单复杂

结论

  • 容忍丢包:UDP快
  • 需要可靠性:TCP快(内核优化 > 应用层实现)
  • 兼顾两者:QUIC

题目:什么场景用UDP,什么场景用TCP?

答案

用UDP

场景原因
视频直播丢几帧无所谓,实时性优先
游戏位置更新频繁,丢包影响小
VoIP(语音通话)丢几个音节可接受
DNS查询一次性查询,数据量小
广播/组播TCP不支持

用TCP

场景原因
文件传输必须100%可靠
HTTP/HTTPS网页内容不能丢
数据库连接数据不能丢
聊天(文本)消息不能丢
邮件内容不能丢

用QUIC

场景原因
HTTP/3快速握手 + 可靠传输
移动网络支持连接迁移(4G切WiFi)

选型建议

  • 默认:TCP(可靠、成熟)
  • 实时性优先:UDP
  • 既要快又要可靠:QUIC(HTTP/3)

🎉 结束语

一周后,哈吉米把消息推送改回了TCP。

哈吉米:"需要可靠性的场景,TCP比自己实现的可靠UDP快得多!"

南北绿豆:"对,TCP经过40年优化,内核实现比应用层实现快。"

阿西噶阿西:"记住:UDP不是银弹,需要可靠性就用TCP,需要极致实时性才用UDP。"

哈吉米:"还有QUIC协议,兼顾了UDP的速度和TCP的可靠性,HTTP/3就是基于QUIC。"

南北绿豆:"对,理解了UDP和TCP的差异,才知道如何选型!"


记忆口诀

UDP快在无握手挥手,短连接场景优势大
长连接差距不明显,TCP内核优化好
需要可靠性别用UDP,自己实现比TCP慢
视频游戏容忍丢包,UDP实时性优先
文件传输聊天用TCP,QUIC兼顾快可靠