☰
【计算基础|网络13】UDP 与可靠传输
2026/10/14 5:31:53 网站建设 项目流程

【计算基础|网络13】UDP 与可靠传输

TCP 讲了两篇,好像什么好事都被它占了:可靠、按序、流控、拥塞控制。那 UDP 凭什么还活着?DNS 用它、视频通话用它、游戏用它、QUIC 还在它上面盖了 HTTP/3——一个"不可靠、无连接、不保证顺序"的协议,怎么就成了现代网络的顶流?

这篇把 UDP 讲透:首部就 8 字节长什么样、为什么不可靠还满世界用、怎么在 UDP 上自己实现可靠传输(QUIC 就是这么干的),以及一个面试必问题——TCP 和 UDP 能不能共用一个端口。全部用 Python 实测,包括一个带丢包重传的简化可靠 UDP 协议。

环境:macOS 27、Python 3.14、标准库socket。不需要装任何第三方包。


一、UDP 是什么

结论先给:UDP 是一个"尽力而为"的无连接传输层协议,首部只有 8 字节,把数据报丢出去就不管了。

特性说明
无连接发数据前不用握手,sendto直接发
不可靠不保证到达、不保证顺序、不保证不重复
面向报文你发多长,它就发多长,不会拆也不会粘
尽最大努力交付能发就发,丢了也不重传
首部开销小只有 8 字节(TCP 首部至少 20 字节)

1.1 UDP 首部逐字段拆解

0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | 源端口 | 目的端口 | ← 各 2 字节 +--------+--------+--------+--------+ | 长度 | 校验和 | ← 各 2 字节 +--------+--------+--------+--------+ | 数据(可选) | +------------------------------------+
字段长度作用
源端口2 字节发送方端口,不需要时可以填 0
目的端口2 字节接收方端口
长度2 字节UDP 首部 + 数据的总长度
校验和2 字节校验首部和数据是否出错;IPv4 中可以为 0(不校验),IPv6 中强制

🔴对比 TCP 首部:TCP 至少 20 字节,还要加序号、确认号、窗口、选项……UDP 就这 4 个字段,8 字节。这就是为什么 DNS 这种"一问一答"的场景用 UDP——省掉握手和确认的时间,一个包就搞定。

1.2 UDP vs TCP 对比表

维度UDPTCP
连接无连接面向连接(三次握手)
可靠性不保证保证可靠交付
顺序不保证保证按序
流量控制无有
拥塞控制无有
首部开销8 字节20+ 字节
传输效率高(无握手、无确认)相对低
传输模式面向报文(保留边界)面向字节流(会粘包)
多播/广播支持不支持
适用场景实时、可容忍丢包要求准确传输

二、UDP 为什么不可靠还满世界用

"不可靠"听起来像缺陷,但很多场景下不可靠恰恰是优点:

场景为什么用 UDP
视频直播/语音通话丢一帧无所谓,卡一下比延迟高更难受
在线游戏位置信息丢一帧,下一帧就覆盖了,不用重传
DNS 查询一个请求一个响应,三次握手的时间比查询本身还长
DHCP刚开机还没 IP,只能广播找 DHCP 服务器
SNMP 网管设备多,偶尔丢一个不影响整体监控
QUIC / HTTP/3在 UDP 上自己实现可靠 + 多路复用,解决 TCP 队头阻塞

🔴核心思路:UDP 把"可不可靠"的选择权交给应用层。业务能容忍丢包就直接用;不能容忍就自己在上面实现可靠——QUIC 就是这么干的。


三、UDP 编程实战

3.1 一个最小的 UDP echo

服务端:

importsocket s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)# ← SOCK_DGRAM 就是 UDPs.bind(("127.0.0.1",9200))print("UDP echo 服务端 127.0.0.1:9200")whileTrue:data,addr=s.recvfrom(1024)# 注意:返回 (数据, 对端地址)print(f"收到来自{addr}:{data.decode('utf-8',errors='replace')!r}")s.sendto(b"echo:"+data,addr)# 必须指定对端地址

客户端:

importsocket s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.settimeout(2)formsgin[b"hello","你好 UDP".encode(),"再发一条".encode()]:s.sendto(msg,("127.0.0.1",9200))# 必须指定地址try:data,addr=s.recvfrom(1024)print(f"发:{msg!r}-> 收:{data!r}")exceptsocket.timeout:print(f"发:{msg!r}-> 超时无响应")s.close()

✅ 实测输出:

UDP echo 服务端 127.0.0.1:9200 发: b'hello' -> 收: b'echo:hello' 发: b'\xe4\xbd\xa0\xe5\xa5\xbd UDP' -> 收: b'echo:\xe4\xbd\xa0\xe5\xa5\xbd UDP' 发: b'\xe5\x86\x8d\xe5\x8f\x91\xe4\xb8\x80\xe6\x9d\xa1' -> 收: b'echo:\xe5\x86\x8d\xe5\x8f\x91\xe4\xb8\x80\xe6\x9d\xa1'

3.2 UDP 编程三个易错点

⚠️易错点 1:UDP 没有"连接"概念,sendto每次都要指定对端地址,recvfrom每次都要接收返回的地址。如果你觉得每次写地址很烦,可以connect()一下,之后就能用send/recv了——但这不是真的建立连接,只是内核帮你记住默认对端。

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.connect(("127.0.0.1",9200))# 不是真连接,只是绑个默认地址s.send(b"hello")# 之后用 send 不用 sendtoprint(s.recv(100))

⚠️易错点 2:UDP 报文最大长度受 MTU 限制。理论上限是65535 - 8(UDP首部) - 20(IP首部) = 65507字节,但实际链路 MTU 一般是 1500 字节,超过会被 IP 层分片。分片在网络中任何一片丢了,整个 UDP 包都废了。所以 UDP 应用一般控制在 1472 字节以内。

⚠️易错点 3:IPv4 中 UDP 校验和可以为 0(不校验),IPv6 中强制校验。别依赖 UDP 校验和做应用层校验。


四、在 UDP 上实现可靠传输

UDP 不保证可靠,但业务需要可靠怎么办?答案:自己在 UDP 上加一层可靠协议。QUIC 就是这么干的。我们写一个简化版,看清它需要解决哪几个问题。

4.1 要解决的四个问题

问题TCP 怎么解决我们简化版怎么做
丢包检测超时 + 重复 ACK发送方设超时定时器,没收到 ACK 就重传
重传超时重传 + 快速重传超时重传
乱序接收缓冲区按序交付接收方缓存,按序号交付
累积确认ACK 号 = 下一个期望的序号同样:ACK = 期望的下一个序号

4.2 报文格式

我们定义一个极简头部:

0 7 8 31 +--------+------------+ | type | seq(4B) | ← type: 0=DATA, 1=ACK, 2=FIN +--------+------------+ | length(2B) | payload | +------------+---------+

4.3 完整实现(带 30% 模拟丢包)

""" 简化版可靠 UDP: - 报文: type(1B) | seq(4B, 大端) | length(2B, 大端) | payload - 客户端发数据,服务端回 ACK;客户端超时重传 - 服务端模拟 30% 丢包,验证重传机制 """importsocketimportstructimporttimeimportrandomimportthreading SERVER=("127.0.0.1",9300)LOSS_RATE=0.30TIMEOUT=0.5defpack(type_,seq,payload):returnstruct.pack("!BHI",type_,len(payload),seq)+payloaddefunpack(buf):type_,length,seq=struct.unpack("!BHI",buf[:7])returntype_,seq,buf[7:7+length]defserver():s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.bind(SERVER)s.settimeout(2)expected=0received=[]print(f"[server] 启动,模拟丢包率{LOSS_RATE*100:.0f}%")deadline=time.time()+8whiletime.time()<deadline:try:buf,addr=s.recvfrom(2048)exceptsocket.timeout:continuetype_,seq,payload=unpack(buf)iftype_==2:print(f"[server] 收到 FIN, 共交付{len(received)}个包")s.sendto(pack(2,0,b""),addr)breakiftype_!=0:continue# 模拟丢包ifrandom.random()<LOSS_RATE:print(f"[server] 丢弃 DATA seq={seq}(模拟丢包)")continueifseq==expected:received.append(payload)expected+=1print(f"[server] 交付 DATA seq={seq}payload={payload!r}")else:print(f"[server] 乱序/重复 seq={seq}, 期望{expected}, 仍回 ACK={expected}")s.sendto(pack(1,expected,b""),addr)s.close()defclient():s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.settimeout(TIMEOUT)data=[f"chunk-{i:02d}".encode()foriinrange(6)]base=0total=len(data)whilebase<total:forseqinrange(base,min(base+2,total)):s.sendto(pack(0,seq,data[seq]),SERVER)print(f"[client] 发送 DATA seq={seq}")try:buf,_=s.recvfrom(2048)type_,ack,_=unpack(buf)iftype_==1:print(f"[client] 收到 ACK={ack}")base=ackexceptsocket.timeout:print(f"[client] 超时,重传 base={base}")s.sendto(pack(2,0,b""),SERVER)s.close()print("[client] 全部发送完成")if__name__=="__main__":random.seed(42)threading.Thread(target=server,daemon=True).start()time.sleep(0.3)client()time.sleep(1)

4.4 实测输出(30% 丢包)

✅ 跑一遍,重点看重传怎么救回来的:

[server] 启动,模拟丢包率 30% [client] 发送 DATA seq=0 [server] 交付 DATA seq=0 payload=b'chunk-00' [client] 发送 DATA seq=1 [server] 丢弃 DATA seq=1 (模拟丢包) [client] 收到 ACK=1 [client] 发送 DATA seq=1 [server] 丢弃 DATA seq=1 (模拟丢包) [client] 发送 DATA seq=2 [server] 丢弃 DATA seq=2 (模拟丢包) [client] 超时,重传 base=1 [client] 发送 DATA seq=1 [client] 发送 DATA seq=2 [server] 交付 DATA seq=1 payload=b'chunk-01' [server] 交付 DATA seq=2 payload=b'chunk-02' [client] 收到 ACK=2 ... [server] 收到 FIN, 共交付 6 个包 [client] 全部发送完成

🔴 看清楚了:中间丢了 5 个包,全部通过超时重传补回来了,最终 6 个包一个不少按序交付。这就是在 UDP 上实现可靠传输的核心——序号 + 确认 + 超时重传,跟 TCP 的思路一模一样,只是从内核搬到了应用层。

4.5 发送/接收流程图

服务端客户端服务端客户端seq=1 丢了DATA seq=0DATA seq=1ACK=1(确认 0 已到)超时定时器响重传 DATA seq=1ACK=2(1 和 0 都到了)DATA seq=2ACK=3

五、QUIC:UDP 上的现代可靠协议

5.1 QUIC 是什么

QUIC 是 Google 提出的传输层协议,跑在 UDP 上,是 HTTP/3 的底层传输。它解决了 TCP 的几个老问题:

TCP 的问题QUIC 怎么解决
队头阻塞:一个包丢了,后面的包全卡住传输层多路复用,不同 stream 之间互不影响
握手慢:TCP 三次握手 + TLS 握手要 2-3 RTT0-RTT/1-RTT就能建连
连接迁移:手机从 WiFi 切到 4G,IP 变了连接就断用 Connection ID 标识连接,IP 变了连接不断
TLS 不在内核内置 TLS 1.3,加密是默认
拥塞控制改不动应用层实现,升级快

5.2 QUIC 怎么在 UDP 上做到可靠

跟我们上面写的 demo 思路一样,只是工程化了:

  1. 自己的序号空间:每个 stream 独立编号,丢一个 stream 的包不影响其他 stream
  2. 自己的确认机制:选择性 ACK(SACK),不用像 TCP 那样傻等
  3. 自己的重传:按 stream 独立重传
  4. 自己的流控和拥塞控制:应用层实现,能快速迭代

🔴一句话:QUIC = UDP 上的一个用户态 TCP,但比 TCP 更现代(多路复用、0-RTT、连接迁移)。


六、TCP/UDP 能不能共用一个端口

能!这是面试高频题,答案是肯定的。我们直接实测。

6.1 原理

TCP 和 UDP 是两个独立的传输层协议,各自维护自己的端口空间。同一个端口号,TCP 和 UDP 各用各的,内核通过五元组区分连接:

(协议, 源IP, 源端口, 目的IP, 目的端口)

协议不同(TCP vs UDP),五元组就不同,就是不同的 socket,不冲突。

6.2 实测代码

importsocketimportthreadingimporttime PORT=9999# ① TCP 服务端tcp_srv=socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_srv.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)tcp_srv.bind(("127.0.0.1",PORT))tcp_srv.listen(1)print(f"[TCP] 监听 127.0.0.1:{PORT}")# ② UDP 服务端(同一端口)udp_srv=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)udp_srv.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)udp_srv.bind(("127.0.0.1",PORT))print(f"[UDP] 监听 127.0.0.1:{PORT}")deftcp_demo():c=socket.socket()c.connect(("127.0.0.1",PORT))c.sendall(b"via-tcp")conn,_=tcp_srv.accept()print("[TCP] 服务端收到:",conn.recv(100))conn.close();c.close()defudp_demo():c=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)c.sendto(b"via-udp",("127.0.0.1",PORT))data,_=udp_srv.recvfrom(100)print("[UDP] 服务端收到:",data)c.close()threading.Thread(target=tcp_demo).start()threading.Thread(target=udp_demo).start()time.sleep(1)print("结论:TCP 9999 和 UDP 9999 可以同时绑定,内核按协议区分")

✅ 实测输出:

[TCP] 监听 127.0.0.1:9999 [UDP] 监听 127.0.0.1:9999 [UDP] 服务端收到: b'via-udp' [TCP] 服务端收到: b'via-tcp' 结论:TCP 9999 和 UDP 9999 可以同时绑定,内核按协议区分

🔴真实例子:DNS 的 53 端口就是同时用 TCP 和 UDP 的——小查询走 UDP,区域传送(数据大)走 TCP。


七、UDP 编程注意事项速查

注意点说明
recvfrom返回(数据, 对端地址)跟 TCP 的recv只返回数据不一样
sendto必须指定对端地址或者先connect()绑定默认地址
UDP 报文最大长度理论 65507 字节,实际建议 ≤ 1472(MTU 1500 减 IP/UDP 首部)
UDP 校验和IPv4 可选(0 表示不校验),IPv6 强制
UDP 不会粘包每次sendto就是一个独立报文,recvfrom一次读一个完整报文
丢包应用层自己处理内核不会帮你重传

总结:你真正需要记住的 7 件事

  1. UDP 首部只有 8 字节(源端口/目的端口/长度/校验和),比 TCP 轻太多。
  2. UDP 无连接、不可靠、面向报文,但视频、游戏、DNS 都用它——因为可容忍丢包 + 不需要握手。
  3. 需要可靠就在应用层自己加:序号 + ACK + 超时重传,QUIC 就是这么干的。
  4. UDP 编程三剑客:SOCK_DGRAM、sendto/recvfrom、地址每次都要带。
  5. QUIC = UDP 上的现代 TCP:多路复用、0-RTT、连接迁移、内置 TLS 1.3。
  6. TCP 和 UDP 可以共用同一个端口,五元组里"协议"不同就是不同 socket。
  7. DNS 53 端口同时跑 TCP 和 UDP就是最经典的例子。

验证清单

  • 能默写出 UDP 首部的四个字段
  • 能说清 UDP 和 TCP 在连接/可靠性/首部/顺序上的区别
  • 能写一个 UDP echo 服务端和客户端并跑通
  • 能说出为什么 UDP 不需要握手还能用来做 DNS
  • 能说清在 UDP 上实现可靠传输需要哪几个机制
  • 能解释 QUIC 为什么跑在 UDP 上而不是自己做新的传输协议
  • 能回答"TCP 和 UDP 能不能共用端口"并给出原理
  • 能说出 UDP 报文实际建议的最大长度是多少、为什么

参考资源

  • RFC 768:User Datagram Protocol https://www.rfc-editor.org/rfc/rfc768
  • Python socket 官方文档 https://docs.python.org/3/library/socket.html
  • QUIC 协议 RFC 9000 https://www.rfc-editor.org/rfc/rfc9000
  • MDN:UDP 协议解释 https://developer.mozilla.org/zh-CN/docs/Glossary/UDP
  • Cloudflare:什么是 QUIC? https://www.cloudflare.com/learning/performance/what-is-quic/
  • QUIC 官方 Wiki(Chromium 项目)https://www.chromium.org/quic/

下一篇《【计算基础|网络14】IP 与 ICMP:路由怎么选、ping 到底在干嘛》讲:IP 首部的关键字段、IP 地址怎么分类(A/B/C/D/E)、公网和私网怎么区分、子网掩码怎么算、ping(ICMP Echo Request/Reply)和 traceroute 到底利用了 IP 的什么机制,以及 NATS 穿透的基本思路。

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

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

立即咨询