TCP 与 UDP

TCP 和 UDP 怎么选:三次握手和 TLS 各占几个 RTT、丢包重传为什么会让延迟跳到秒级、TIME_WAIT 和临时端口耗尽、队头阻塞,以及 QUIC / HTTP/3 如何在 UDP 上重做可靠传输。带跨洲 RTT 算例、翻车表和面试答法。

选传输层协议要回答三个问题:丢了的数据还要不要、晚到的数据还有没有用、建连和重传的往返能不能承受。 TCP 把可靠、有序做进内核,代价是握手和重传的等待;UDP 什么都不保证,可靠性要应用自己做,QUIC 就是在 UDP 上把这件事重新做了一遍。

Trade-off 快览

  • TCP:可靠、有序、重传,适合对完整性要求高的场景
  • UDP:更快、低开销,但可能丢包,适合实时与低 latency 场景

示例:

  • email / 文件传输 → TCP
  • 视频直播 / online gaming → UDP

有约束的设计问题

一个在线教育 App,服务端在美西,大量用户在悉尼。要传的东西有四类:

  1. 课程页面和 API:每个字节都要到,顺序不能错。
  2. 课后作业上传:几十 MB,可以慢一点,但不能坏。
  3. 直播课的音视频:晚到 300 ms 的帧已经没用了,宁可丢掉。
  4. 客户端查域名的 DNS 请求:一问一答,很小。

四类流量的答案不同,下文每一节都回到它们。

TCP

Transmission Control Protocol (TCP) 是 connection-oriented 的协议,一旦 connection 建立,数据就可以双向传输。TCP 自带 error-checking 和顺序保证,能保证数据按发送顺序到达,适合传输静态图片、data files、web pages 等内容。

tcp

TCP 很可靠,但它的反馈机制会带来更大的 overhead,占用更多 network 带宽。这份 overhead 在延迟上具体体现为下面几处。

建连:三次握手

客户端发 SYN,服务端回 SYN-ACK,客户端再发 ACK(RFC 9293)。第三个 ACK 可以和第一段数据一起发,所以握手本身花 1 个 RTT。HTTPS 还要在其上做 TLS:TLS 1.3 完整握手 1 个 RTT(RFC 8446),TLS 1.2 是 2 个 RTT。

算一下场景里的悉尼用户。以下 RTT 是假设值:悉尼到美西按 150 ms 算。

协议组合首字节前的往返约耗时
TCP + TLS 1.2 + 请求1 + 2 + 1 = 4 RTT600 ms
TCP + TLS 1.3 + 请求1 + 1 + 1 = 3 RTT450 ms
QUIC(新连接)+ 请求1 + 1 = 2 RTT300 ms
QUIC 0-RTT 恢复 + 请求1 RTT150 ms

这就是为什么连接复用(HTTP keep-alive、连接池)比任何参数调优都重要:复用的连接没有握手,只剩请求本身的 1 个 RTT。TCP Fast Open(RFC 7413)允许在 SYN 里带数据,但需要此前连过同一服务端拿到 cookie,RFC 7413 也讨论了中间设备丢弃这类 SYN 的情况,客户端要能回退到普通握手。

丢包:重传超时是秒级的

TCP 靠 ACK 发现丢包。快速重传在收到重复 ACK 时就补发,代价约 1 个 RTT;如果后面没有足够的包触发重复 ACK(比如丢的是 SYN,或者是一次响应的最后几个包),就只能等重传超时(RTO):

  • RFC 6298 规定初始 RTO 为 1 秒,计算出的 RTO 小于 1 秒时 SHOULD 取 1 秒。
  • Linux 没照这个下限,内核源码 include/net/tcp.h 里 TCP_RTO_MIN 是 HZ/5,即 200 ms;TCP_RTO_MAX 是 120 秒。
  • SYN 丢了走的是初始 RTO。Linux 文档里 tcp_syn_retries 默认 6 次,当前内核在最后一次 SYN 重传后大约 131 秒才判定连接失败。

所以「建连偶尔要 1 秒多」通常就是一个 SYN 或 SYN-ACK 丢了。应用层的 connect timeout 如果设成 1 秒,正好会在第一次重传前放弃;设成 3 秒则能容忍一次丢包。

慢启动:新连接一开始跑不满带宽

新连接的初始拥塞窗口按 RFC 6928 是 10 个 segment(约 14.6 KB),之后每个 RTT 大致翻倍。理想情况下(无丢包、MSS 1460)传一个 100 KB 的响应:第 1 个 RTT 发 14.6 KB,第 2 个 29.2 KB,第 3 个 58.4 KB,累计 102.2 KB,要 3 个 RTT。在 150 ms RTT 下这就是 450 ms,和带宽有多大无关。Linux 的 tcp_slow_start_after_idle 默认开启,空闲一个 RTO 后窗口会被重置,长连接闲置后的第一个大响应又要重新爬坡。

关连接:TIME_WAIT 和临时端口

主动关闭的一方进入 TIME_WAIT,等 2 倍 MSL 才释放四元组,防止旧连接的迟到包混进新连接。RFC 9293 把 MSL 取为 2 分钟;Linux 把 TIME_WAIT 时长写死为 60 秒(TCP_TIMEWAIT_LEN)。

这对「服务 A 不停新建短连接调服务 B」很要命。Linux 默认临时端口范围 ip_local_port_range 是 32768–60999,共 28,232 个。同一个源 IP 连同一个目标 IP:port,每条连接关闭后端口要占 60 秒:

28,232 个端口 / 60 秒 ≈ 470 条新连接/秒

超过这个速率,connect() 开始报 EADDRNOTAVAIL(Cannot assign requested address)。修法按优先级:用连接池复用长连接;扩大端口范围;增加目标 IP(多个后端地址);tcp_tw_reuse 当前内核默认是 2(只对 loopback 开启),文档注明没有专家建议不应修改。

# 看 TIME_WAIT 数量和端口范围
ss -s
ss -tan state time-wait | wc -l
sysctl net.ipv4.ip_local_port_range

队头阻塞

TCP 只交付一条有序字节流。HTTP/2 在一条 TCP 连接上复用多个请求,但只要丢一个包,后面所有请求的数据都得等这个包重传完才能交给应用,即使它们属于别的 stream。从机制上推,丢包率越高,HTTP/2 单连接越可能比 HTTP/1.1 的多条并行连接更慢。

UDP

User Datagram Protocol (UDP) 是更简单的 connectionless 协议,不做重传和排序。UDP 不需要建立/维护/终止 connection 的 overhead,数据会持续发送给接收方,不管对方是否真的收到。UDP 头只有 8 字节(RFC 768),带一个校验和,校验失败的包直接丢弃,不会重发。

udp

UDP 常用于 real-time 通信,也支持 broadcast / multicast。当你更在意 最低 latency,且「晚到的数据比丢数据更糟」时,应优先用 UDP。

用 UDP 意味着这些事要应用自己决定:

  • 丢包:音视频用前向纠错或直接跳过;需要可靠的消息自己加序号和重传。
  • 乱序:用序号或时间戳丢弃过时的包,比如游戏只保留最新的位置快照。
  • 拥塞控制:UDP 不会自己降速,发得太猛会把链路和别人的流量一起挤垮。
  • NAT 与防火墙:没有连接状态,NAT 映射靠超时维护,长时间不发包映射会被回收,所以实时应用会定期发 keepalive。

DNS 是 UDP 的经典用法:一问一答,丢了就重发整个查询。RFC 1035 把 UDP 上的 DNS 消息限制在 512 字节,超长响应设置截断位(TC),客户端改用 TCP 重查;EDNS(0)(RFC 6891)允许协商更大的 UDP 报文。RFC 7766 要求 DNS 实现必须支持 TCP,所以防火墙只放行 53/UDP 是错误配置。

QUIC 与 HTTP/3:在 UDP 上重做可靠传输

QUIC(RFC 9000)跑在 UDP 上,在用户态实现了可靠传输、拥塞控制和多路复用,并把 TLS 1.3 嵌进握手(RFC 9001)。和 TCP + TLS 相比有三处实质区别:

  1. 握手合并:传输和加密握手一起完成,新连接 1 个 RTT;恢复连接可用 0-RTT 直接发请求。0-RTT 数据可能被重放,只应放幂等请求。
  2. Stream 相互独立:HTTP/3(RFC 9114)每个请求一个 QUIC stream,RFC 原文是一个 stream 阻塞或丢包不会妨碍其他 stream 前进,TCP 层的队头阻塞没有了。
  3. 连接迁移:连接用 connection ID 标识,不绑定四元组。手机从 Wi-Fi 切到 4G,IP 变了连接还在;TCP 连接在这种情况下会断掉重连。

代价:UDP 在一些企业网络里被限速或屏蔽,所以浏览器一般先走 TCP,通过 Alt-Svc 发现服务端支持 HTTP/3 后再切换;QUIC 多在用户态实现,同样流量的 CPU 开销一般高于内核 TCP(这是部署经验,不是 RFC 结论,各实现差异很大,要自己压测)。

TCP vs UDP

TCP 是 connection-oriented;UDP 是 connectionless。TCP 首包更慢(握手)、丢包时更慢(等重传),这些等待换来的是可靠有序。UDP 更简单、开销更低,但丢包重传只在 TCP 里由协议提供,UDP 上要应用自己做。

TCP 提供 ordered delivery,而 UDP 不保证 end-to-end 顺序,也不会检查 receiver readiness。

FeatureTCPUDP
Connection需要先建立连接(1 RTT 握手)Connectionless
Guaranteed delivery保证送达或报错不保证
Re-transmission协议自动重传无,需应用自己实现
Ordering有序字节流每个 datagram 独立,可能乱序
Header20–60 字节8 字节
Congestion control内核内置无,需应用自己实现
Broadcasting不支持支持(IPv4 broadcast / multicast)
Use casesHTTP/1.1、HTTP/2、SMTP、数据库连接DNS、VoIP、直播、游戏、QUIC / HTTP/3

回到场景

流量选择原因
页面与 APITCP + TLS 1.3,长连接复用;可加 HTTP/3必须完整有序;跨洲 RTT 大,握手次数决定首屏
作业上传TCP,分片上传 + 断点续传完整性第一;单连接丢包时会被 RTO 卡住,分片可以只重传失败的片
直播音视频UDP(WebRTC 媒体通常走 UDP 上的 SRTP)晚到的帧没用,重传只会增加延迟
DNSUDP,截断时回退 TCP一问一答,重发整个查询比建连便宜

常见翻车

翻车现象修法
服务间每次调用新建连接高峰时 EADDRNOTAVAIL,大量 TIME_WAIT连接池 / keep-alive;按 470 条/秒/目标估算上限
connect timeout 设为 1 秒偶发建连失败,恰好丢一个 SYN 就失败超时覆盖一次 SYN 重传;并在上层做有退避的重试
防火墙只放行 53/UDP大响应(DNSSEC、多 A 记录)解析失败同时放行 53/TCP
UDP 应用没有拥塞控制带宽跑满时丢包率飙升,同链路其他业务受影响用现成协议(QUIC、WebRTC 的拥塞控制),不要裸发
长连接闲置后首个大响应慢p99 偶发多出几个 RTT了解 tcp_slow_start_after_idle 的影响,按压测结果决定
企业网屏蔽 UDP 443HTTP/3 连不上保留 TCP 回退,不要只提供 HTTP/3

面试时这样回答

  1. 复述约束:「这类数据丢了能不能接受、晚到还有没有用、RTT 大概多少。」
  2. 给出选择:需要完整有序就 TCP;实时媒体、游戏状态用 UDP,并说清谁来处理丢包和拥塞。
  3. 用 RTT 说延迟:「TCP 握手 1 RTT,TLS 1.3 再 1 RTT,跨洲 150 ms 的话首字节至少 450 ms,所以要连接复用或者 HTTP/3。」
  4. 说一个故障:短连接打满临时端口(28,232 个端口 / 60 秒 TIME_WAIT ≈ 470 条/秒),修法是连接池。
  5. 加分项:HTTP/2 的 TCP 队头阻塞,HTTP/3 用 QUIC 的独立 stream 解决;QUIC 0-RTT 只能放幂等请求。

相关章节:OSI Model、IP、DNS、SSL / TLS / mTLS、Long Polling / WebSockets / SSE、Networking Essentials、CDN。

一手证据