TCP与UDP辨析

Star Dust Lv1

TCP 和 UDP:别再因为选错协议半夜爬起来改 Bug 了

你肯定碰到过这种魔幻时刻:自己电脑上跑得六亲不认的网络模块,一扔上服务器就变得脆弱无比——消息莫名其妙短一截,端口用着用着就没了,延迟时好时坏像在蹦迪。多数时候,元凶就藏在传输层的两个“老伙计”身上:TCP 和 UDP。它们一个像偏执狂一样确保每粒字节送达,另一个洒脱到丢包都懒得通知你。搞懂它们的脾气,很多诡异问题会自行消失。


一、TCP:事无巨细的“可靠强迫症”

如果把网络传输比作寄快递,TCP 就是那种寄一封挂号信,先打电话确认地址,途中隔三岔五问“到了吗”,最后还让你签字画押的快递员。它提供的服务叫可靠的字节流,可靠到让你觉得它有点烦,但账本、订单这类东西又离不开它。

1.1 你好、我好、连接好:三次握手与四次挥手

TCP 建立连接要走“三次握手”,释放连接要走“四次挥手”。流程如下:

sequenceDiagram
    participant C as 客户端
    participant S as 服务端
    Note over C,S: 三次握手
    C->>S: SYN (seq=x)
    S->>C: SYN+ACK (seq=y, ack=x+1)
    C->>S: ACK (seq=x+1, ack=y+1)
    Note over C,S: 连接建立完成

    Note over C,S: 四次挥手
    C->>S: FIN (seq=u)
    S->>C: ACK (seq=v, ack=u+1)
    Note right of S: 半关闭状态
    S->>C: FIN (seq=v, ack=u+1)
    C->>S: ACK (seq=u+1, ack=v+1)
    Note left of C: TIME_WAIT (等2MSL)

问题往往出在挥手之后。主动关闭的一方会进入 TIME_WAIT 状态,时长 2 倍 MSL(通常 60 秒)。这段时间里,那个连接占用的本地端口无法复用。短连接一多,netstat 里铺天盖地的 TIME_WAIT 能把端口号吃干抹净,新的连接只能干瞪眼。想缓解的话,要么启用 tcp_tw_reuse(还得同时打开 timestamps),要么直接用长连接或连接池,别让连接动不动就挥来挥去。

1.2 确认、重传、窗口、拥塞——四大金刚

TCP 为了“可靠”二字,祭出了一套组合拳:

  • 确认与重传:收方乖乖回 ACK,发方超时没收到就重传。如果发方一连收到三个重复 ACK(说明后面的包到了但中间缺了),它会立刻重传,不等超时,这招叫“快速重传”。
  • 滑动窗口:收方告诉发方自己还能吃下多少数据(窗口大小),发方据此控制发送速率,避免把收方撑死。
  • 拥塞控制:网络堵车时,TCP 会主动缩小自己的“拥塞窗口”(cwnd),算法包含慢启动、拥塞避免、快速恢复等。常见事故:丢包一多,cwnd 断崖式下跌,吞吐量跌成狗,查半天才发现是底层拥塞算法在“好心地”帮你降速。

1.3 字节流的坑:粘包这件小事

TCP 眼里没有“消息”的概念,只有连绵不断的字节流。你调用两次 send,它可能把这两次的数据拼成一个 TCP 段发出去;也可能拆成三次,全看心情。上层应用如果不做消息切分,接收方就会把两个消息读成一个,或者只读到半截——这就是臭名昭著的“粘包”。

解决方法不外乎三种:

策略做法适合场景
固定长度每消息固定 N 字节,不足补零消息长度极端一致,如传感器数值
分隔符\r\n 等特殊符号作为消息边界文本协议,比如 SMTP
长度前缀消息前加 4 字节声明本次消息体多长二进制协议,最为通用

比如这串代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import struct
import socket

def recv_exactly(sock: socket.socket, n: int) -> bytes:
"""从套接字收取恰好 n 字节数据"""
buf = b''
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("连接在读取时断开")
buf += chunk
return buf

def read_frame(sock: socket.socket) -> bytes:
"""读取一个长度前缀的完整消息帧"""
raw_len = recv_exactly(sock, 4)
msg_len = struct.unpack('!I', raw_len)[0] # 网络字节序大端
return recv_exactly(sock, msg_len)

这段代码没有半点“伪”的,直接拷贝到你的项目里,配合 send 时也先发送 struct.pack('!I', len(body)) 就行。从此粘包问题跟你形同陌路。

1.4 队头阻塞——TCP 的死穴

TCP 是严格有序的,一个报文段丢了,后面所有到达的包都得在内核缓冲区里排队,等那个丢掉的家伙重传到位,应用才能接着读。这就是队头阻塞(Head-of-Line Blocking)。它对于实时通信简直是噩梦:一丢包,延迟就暴增。这也是为什么 HTTP/3 干脆抛弃 TCP,在 UDP 之上搞了 QUIC。


二、UDP:没心没肺,快得纯粹

UDP 是传输层的“极简主义者”,只做了两件事:给数据加上端口号,然后尽力把它扔向目标。不保证送达、不保证顺序、不保证不重复,甚至连连接的概念都没有。

sequenceDiagram
    participant A as 发送方
    participant B as 接收方
    A->>B: 数据报 1 (直接发送)
    A->>B: 数据报 2
    B-->>A: 应用层确认(需要自己实现)
    Note over A,B: UDP 本身不提供任何保证

如果把 TCP 比作查岗式的微信电话,UDP 就是往群里扔了一条语音,对方听没听见、听见几条,完全随缘。正因如此,它几乎没有状态维护开销,一台服务器扛几十万个 UDP 端点轻松愉快。

2.1 快有快的代价:丢包、乱序你得自己扛

游戏里一个角色位置更新包丢了,无所谓,因为 50ms 后又来一包新的。但对可靠性有要求的场景,就必须在应用层自己补课。常见手段:

  • 序列号 + 滑动窗口:每个包带递增序号,接收方只确认连续收到的最大序号,发送方根据确认情况选择性重传。
  • 前向纠错(FEC):发送时附上冗余数据,丢了可以通过算数恢复,避免重传带来的延迟,直播领域很常见。
  • 用现成轮子:KCP、ENet、QUIC 等协议都在 UDP 之上封装了可靠传输,比你自己造轮子靠谱多了。

2.2 包大小:别让 IP 分片出来捣乱

一个 UDP 数据报能塞多少数据?理论上最大 65535 字节,但实际上链路层的 MTU(一般是 1500 字节)才是硬杠杠:

1
安全大小 = MTU - IP头(20) - UDP头(8) = 1472 字节

超过这个数字,IP 层就会将数据报拆成多个碎片。只要其中一片丢了,整个数据报就废了;而且很多老旧防火墙看到分片就直接扔掉,丢包率急剧升高。实际编码时,最好控制在 1400 字节以内,或者自己在应用层拆包。

一个简单的安全发送示例:

1
2
3
4
5
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = b'x' * 1400 # 别超过 1400 字节
sock.sendto(payload, ('example.com', 9999))

2.3 安全问题:UDP 是反射放大攻击的温床

UDP 不验证源地址,伪造起来跟玩似的。攻击者用受害者 IP 发送一个小请求,服务器回一大坨响应,打出一记漂亮的反射放大攻击。所以任何基于 UDP 的应用,都必须在应用层做来源校验,或者套上 DTLS 加密。


三、一图一表,彻览两者差异

抛开抽象的概念,用一张流程图速判选型方向,再用一张表格量化核心区别。

选型速断流程图

graph TD
    A[需要可靠传输?] -->|是| B[数据顺序是否必须严格?]
    A -->|否| C[延迟是否极度敏感?]
    B -->|是| TCP
    B -->|否| D[能否接受队头阻塞?]
    D -->|能| TCP
    D -->|不能| QUIC
    C -->|是| UDP
    C -->|否| E[是否需要连接状态管理?]
    E -->|是| TCP
    E -->|否| UDP

TCP vs UDP 关键差异速查表

维度TCPUDP
连接模型面向连接 (三次握手)无连接,直接发送
可靠性确认+重传,保证交付不保证,尽力而为
数据边界字节流,无边界,易粘包数据报,天然保留边界
顺序保证严格有序无序,应用层自行处理
传输开销较高(ACK、重传、拥塞控制)极低(仅有 8 字节首部)
头部大小20~60 字节8 字节
流控/拥塞控制内建滑动窗口、慢启动等无,由应用自行实现
队头阻塞存在(单个丢包拖慢后续所有数据)无(各数据报独立)
适用场景网页、文件、交易、RPC音视频、游戏、IoT、DNS
常见故障TIME_WAIT 堆积、重传风暴、cwnd 骤降分片丢包、源地址伪造、乱序

四、什么时候该翻谁的牌子?

选协议这事儿,本质是在“可靠性”和“低延迟/低开销”之间做权衡。下面这张表能帮你快速决断:

应用场景推荐理由
网页、文件下载、交易系统TCP字节一个都不能少,顺序也不能乱
微服务间的 RPC 调用TCP (gRPC)可靠、成熟、生态齐全
服务发现、健康检查UDP 组播/单播小报文、周期性,扛得住丢包
视频会议、直播UDP延迟第一,丢几帧不影响体验
多人在线游戏(状态同步)UDP每秒数十次更新,丢了就等下一帧
百万级 IoT 设备心跳UDP没那么多端口和内存维护连接
DNS 查询UDP 为主一次请求一个包搞定,超 512 字节再切 TCP
想同时拥有 TCP 的可靠和 UDP 的速度QUIC (基于 UDP)HTTP/3 的标准传输层,解决队头阻塞,连接迁移丝滑

QUIC 的出现模糊了二者的边界:它在 UDP 之上实现了可靠传输、安全加密和多路复用,还没了队头阻塞。新项目如果追求极致性能,可以直接把目光投向它。


五、写在最后

TCP 和 UDP 没有高低贵贱之分,它们只是网络工具箱里型号不同的两把扳手。理解它们,不是背熟面试八股,而是你能在线上出故障时,看一眼 ss -stcpdump 抓的包,就大致猜到是握手风暴、重传飓风还是数据报被撕碎——然后手起刀落,药到病除。

下一次你的代码要去跟网络打交道时,不妨对着上面的流程图和表格,花两分钟想清楚:这次数据是宁可晚到也不能丢,还是宁可丢几个也不能卡?答案一出,协议自然就选出来了。

  • 标题: TCP与UDP辨析
  • 作者: Star Dust
  • 创建于 : 2026-05-17 21:53:27
  • 更新于 : 2026-05-19 00:01:38
  • 链接: https://starblog.qzz.io/posts/68295e25.html
  • 版权声明: 版权所有 © Star Dust,禁止转载。
评论