UDP 在传输端点之间携带独立数据报,八字节首部包含来源端口、目的端口、长度和校验和字段。它不会加入 TCP 的可靠有序字节流或拥塞控制行为,让应用可以选择自己的交付策略,但不承诺所有负载下延迟都更低。应用确实需要某条内容时,丢失的数据报可能比建立连接的开销更昂贵,必须按完整请求过程评估。

坏掉的样子

发送成功不证明目标应用已经收到或处理消息。UDP 没有内建确认、重传、去重或顺序保证,因此超时不能告诉支付客户端操作是否发生。允许重试时,应用需要能够处理重复请求和结果不确定的协议,不能把没有响应直接解释成业务失败。消息是否到达、动作是否执行、结果是否持久化,也应具有不同含义。

即使本地路径健康,接收缓冲区也能制造失败。数据报大于本次调用提供的缓冲区时,内容可能截断,缺失后缀不会像字节流那样留给下一次读取。Linux 的 recvmsg 可以通过 MSG_TRUNC 报告这一情况。如果把缩短的字节当成完整命令或测量值,就可能在没有明显进程错误的情况下改变业务含义,因此必须检测协议要求的完整性。

UDP 没有内建拥塞控制,意味着应用承担责任,并不意味着可以无限制发送。需要根据部署选择发送节奏、拥塞行为、消息大小和截止时间。分片可能降低大数据报的交付稳健性,过了截止时间的数据也可能已经没有价值。QUIC 等协议能够在 UDP 之上提供可靠流,说明底层载体本身不能决定全部应用可靠性。

快速验证

将脚本保存为 check-udp.py,准备 Docker 后运行 python3 check-udp.py。固定 Python 3.11.16 Linux 容器没有外部网络路径和宿主端口发布。第一部分检查真实套接字接收语义;第二部分使用刻意编写的中继扣留、重复和重排消息,不声称测量自然发生的网络丢包。实验的每个故障输入都在代码中明确可见:

import json,subprocess,uuid
image="python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84"
program=r"""
import json,socket

with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as sender,socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as receiver:
    receiver.bind(('127.0.0.1',0));receiver.settimeout(.25);sender.settimeout(1);target=receiver.getsockname()
    sender.connect(target)
    for body in [b'A',b'BC',b'']:sender.send(body)
    boundaries=[receiver.recvfrom(16)[0] for _ in range(3)]
    assert boundaries==[b'A',b'BC',b'']
    sender.send(b'abcdefgh');data,ancillary,flags,peer=receiver.recvmsg(3)
    assert data==b'abc' and flags&socket.MSG_TRUNC
    try:receiver.recvfrom(16);raise AssertionError('truncated suffix survived')
    except TimeoutError:tail='discarded'
    sender.send(b'next');assert receiver.recvfrom(16)[0]==b'next'

    # A deliberate application relay changes delivery; this is not measured network loss.
    received=[];applied=[];seen=set();forwarded=[]
    with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as relay,socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as output:
        relay.bind(('127.0.0.1',0));relay.settimeout(1);output.settimeout(1)
        sender.connect(relay.getsockname())
        for seq in [1,2,3]:sender.send(str(seq).encode())
        inputs=[relay.recvfrom(16)[0] for _ in range(3)]
        for index in [2,0,0]:
            output.sendto(inputs[index],target);forwarded.append(int(inputs[index]))
            value=int(receiver.recvfrom(16)[0]);received.append(value)
            if value not in seen:seen.add(value);applied.append(value)
        assert received==[3,1,1] and applied==[3,1] and 2 not in seen
    try:receiver.recvfrom(16);raise AssertionError('missing message arrived by itself')
    except TimeoutError:no_retransmit=True
    print(json.dumps({'datagram_lengths':[len(x) for x in boundaries],'empty_datagram_is_message':boundaries[2]==b'',
        'short_buffer':data.decode(),'truncation_flag':bool(flags&socket.MSG_TRUNC),'unread_tail':tail,
        'relay_inputs':[int(x) for x in inputs],'relay_outputs':forwarded,'receiver':received,'deduplicated':applied,
        'missing_sequence':2,'no_automatic_retransmission_during_deadline':no_retransmit}))
"""
name='xbcatm-socket-'+uuid.uuid4().hex[:10]
try:
    result=subprocess.run(['docker','run','--rm','-i','--name',name,'--network','none',
        '--memory','128m','--pids-limit','64',image,'python3','-'],input=program,
        capture_output=True,text=True,timeout=60)
    assert result.returncode==0,result.stderr
    print(json.dumps(json.loads(result.stdout),ensure_ascii=False,sort_keys=True))
finally:
    subprocess.run(['docker','rm','-f',name],capture_output=True,text=True,timeout=10)

三次发送分别包含 A、BC 和空载荷,三次接收得到长度一、二和零。每次读取消费一条数据报,没有把相邻发送合并成字节流。空结果是合法零长度消息,不代表发送方关闭连接。发送套接字使用 UDP connect 选定对端,这个接口调用不会把 UDP 变成 TCP,也不会凭空增加确认和重传机制。

接着发送八字节 abcdefgh,接收方只提供三字节缓冲区,得到 abc 和 MSG_TRUNC。之后读取超时,没有拿到 defgh;再发送新的 next 数据报,接收方能够取得完整新消息。这个正向对照说明旧尾部已经被丢弃,而不是接收套接字在第一次调用后完全失效。截断与后续新消息属于不同数据报生命周期。

中继先收到序号一、二、三,再有意转发三、一、一,省略二。下游真实套接字收到这个选定序列。应用用一个小集合过滤重复值,最终执行序号三和一,却没有恢复原始顺序,也没有重建缺失消息。去重只解决其中一项问题,不能把它当成完整可靠传输或业务事务协议。所需语义必须逐项明确。

之后接收方等待下一条数据报,直到达到截止时间,UDP 没有自动重传序号二。这里缺失由中继制造,不能把结果写成某条网络的可靠性统计。它提供可复现的输入模式,用来观察应用面对传输契约没有排除的交付情况时如何反应。程序行为、故障注入与实际网络测量,需要在结果解释中保持区分。

有意识地补上所需语义

实时测量可能丢弃旧样本并显示最新序号,文件传输则需要恢复缺失内容。应用确认可以表示已经收到,也可以表示操作结果已经持久化,但含义必须由协议定义。对有副作用动作增加重试前,应明确标识符范围、重试截止时间、去重保留期和进程重启行为。本例没有实现这些更完整的事务机制,不能直接承担相应承诺。

同时规划接收能力和单条数据报大小,并把截断、应用发现的缺失与格式错误分别记录。增加套接字队列,无法修复单次接收缓冲区太小的问题;提高发送速率也可能加重拥塞,而不是改善信息新鲜度。应测量多少有用消息在截止时间前到达,而不只统计本地发送调用接受了多少字节。两种数字反映的结果不同。

当消息模型符合所建协议,且缺失的保证都有明确负责机制时,UDP 才是合理选择。以真实端到端时延、失败恢复和资源消耗判断性能,而不是仅凭省略握手就宣称更快。保留边界能简化部分解析,但完整数据报始终不到达、重复到达或到得太迟时,应用仍须具有定义清楚且经过验证的行为。

来源