UDP 保留数据报边界,恢复和时效由应用负责
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,也不会凭空增加确认和重传机制。 ...