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?")

发送方中有几点值得注意:

  • 没有 bind(),也没有 connect()。UDP 客户端只管发送即可。

  • 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 做同样的介绍。