9.12. UDP 套接字¶
在 Python 中,UDP 流量通过数据报套接字上的两个方法来收发:sendto() 用于向选定的目标发出一个数据报,recvfrom() 用于接收一个数据报并查明它来自何处。每次调用都传递一条自包含的消息;不存在连接状态。
9.12.1. 发送数据报¶
最简单的 UDP 发送只需要在套接字构造函数之上写一行 Python 代码:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()
上述代码会把 b"hello" 发送到 192.168.1.20 上的 9000 端口,然后便不再理会。MicroPython 会挑选一个临时的源端口;脚本无需绑定任何东西。
把同一份有效载荷发送给多个目标只需一个循环——套接字在多次发送之间是可复用的,而且不需要建立任何连接:
targets = [
("192.168.1.20", 9000),
("192.168.1.21", 9000),
("192.168.1.22", 9000),
]
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
for addr in targets:
s.sendto(b"hello", addr)
9.12.2. 接收数据报¶
要接收数据报,套接字必须占用一个已知端口,发送方会把该端口作为它们的目标地址。这就是 bind() 调用的作用:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))
while True:
data, src = s.recvfrom(1024)
print("from", src, "got", data)
"0.0.0.0" 地址的意思是“摄像头上的每一个 IPv4 接口”——无论是 Wi-Fi 还是以太网接口传入数据包,9000 端口都归这个套接字所有。
传给 recvfrom() 的 1024 参数表示要读入返回缓冲区的最大字节数。超过此大小的 UDP 数据报将被 截断;请根据应用预期的最大数据报来选取该值。
recvfrom() 返回 (data, src):接收到的字节,以及发送方的地址。发送方的地址正是回复的目标,这使得编写一个简单的请求/响应协议变得很容易:
while True:
request, src = s.recvfrom(1024)
if request == b"ping":
s.sendto(b"pong", src)
默认情况下,recvfrom() 会 阻塞 直到有数据报到达。让它不阻塞的各种方式——超时、非阻塞套接字、asyncio——都在 使用 asyncio 的套接字 中介绍。
9.12.3. 一个请求和一个回复¶
两个简短的脚本:一个发送请求,另一个接收并回复。
接收方:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))
while True:
req, src = s.recvfrom(64)
print("got", req, "from", src)
s.sendto(b"ack: " + req, src)
发送方:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(2.0) # 2 s reply window
s.sendto(b"ping", ("192.168.1.20", 9000))
try:
reply, _ = s.recvfrom(64)
print("reply:", reply)
except OSError:
print("no reply in 2 s -- packet lost?")
发送方中有几点值得注意:
settimeout()为接收调用设定了一个截止时间。如果两秒内没有回复到达,该调用会抛出OSError,而不是永远阻塞下去——这是一种检测丢包的自然方式。with代码块会自动关闭套接字。
9.12.4. 数据报大小限制¶
理论上 UDP 数据报最大可达约 64 KB,但实际限制要小得多。发送方与接收方之间路径上的每一段链路都有一个 最大传输单元(MTU)——即该链路在一帧中能承载的最大单块字节数。以太网和 Wi-Fi 都将其限制在 1500 字节左右,而几乎每一条互联网路径在某处都会回溯到这个限制。
当数据报超过它需要穿越的某段链路的 MTU 时,网络层会把它拆分成更小的 分片,并在目的地重新组装。UDP 本身从不会察觉到这种拆分,但分片有几个令人不便的特性:
如果任何一个分片丢失,整个数据报都会在接收方被丢弃——不存在针对单个分片的重传。丢失概率会随分片数量增加而增大。
有些网络和防火墙会把分片数据包视为可疑而直接丢弃。
重组会在接收方消耗内存,而在微控制器上内存是稀缺资源。
在摄像头上的实用规则是:让 UDP 消息远小于 1500 字节。约 1400 字节可以为 IP 和 UDP 头部、路径增加的任何隧道开销,以及以太网、Wi-Fi 和 VPN 链路之间 MTU 的细微差异留出余地。需要发送超过这个大小数据的应用,要么应在应用层对数据进行分块,要么改用 TCP,后者会自动处理拆分和重组。
9.12.5. 常见陷阱¶
忘记 UDP 可能丢包。 在安静的本地网络上完美运行的代码,有时会在更繁忙或更广域的网络上以微妙的方式失败。请始终为消息可能未送达的情况进行设计。
接收方在发送方发送之前尚未绑定。 发送到无人监听端口的数据报会被静默丢弃。请先启动接收方。
发送的数据报大于路径的 MTU。 参见上一节——让消息保持在约 1400 字节以下。
上述模式涵盖了摄像头几乎所有的 UDP 使用场景。下一页将针对 TCP 做同样的介绍。