Python UDP Socket发送大payload时丢包,约600报文后接收中断
我正在实现一个逻辑数据二极管(数据仅单向传输,不允许ACK),因此选择UDP协议。协议核心流程如下:
- 将负载(payload)拆分为小块
- 为每个块分配序列号(sequence number)
- 通过UDP向接收端传输该块
小负载传输时一切正常,但大负载在发送约600个数据报(datagram)后,报文似乎在传输中神秘丢失:发送端日志显示所有报文已发送,但接收端完全停止接收。
相关代码片段
发送循环代码:
for i in range(redundancy + 1): seq = 0 sent_bytes = 0 bytes_to_send = len(session.encrypted_data) while sent_bytes < bytes_to_send: logger.info("Sending payload chunk %d", seq) header = concat_bytes(str(seq).zfill(16).encode(), session.session_uuid.bytes_le) remaining_room = BUFFER_SIZE - len(header) data = session.encrypted_data[sent_bytes : sent_bytes + remaining_room] payload = concat_bytes(header, data) self._transmit_bytes(payload) sent_bytes += len(data) seq += 1
_transmit_bytes方法:
def _transmit_bytes(self, message: bytes): self.server_socket.sendto(message, self.addr) time.sleep(MESSAGE_DELAY)
server_socket初始化代码:
self.server_socket: LDDSocket = LDDSocket(listen=False) so_linger_options = struct.pack("ii", 1, CLOSE_TIMEOUT) send_buffer_size = 1024 * 1024 * 100 # 100 MB self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, so_linger_options) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, send_buffer_size)
LDDSocket是socket.socket的极简封装(listen=False仅表示不执行bind操作)。
已尝试的优化方案(无效)
- 设置SO_LINGER选项(
CLOSE_TIMEOUT为10秒) - 设置SO_SNDBUF选项(100MB)
- 关闭socket前等待10秒,但接收端远在该超时及后续
socket.close()调用前就已停止接收
发送器类的__exit__方法(上下文管理器):
def __exit__(self, exc_type, exc_val, exc_tb): cleanup_grace_period = 10 # seconds logger.info("Waiting %d seconds for cleanup...", cleanup_grace_period) time.sleep(cleanup_grace_period) logger.info("Cleanup presumed to be complete; closing socket.") self.close() logger.info("Socket closed.")
请问,导致socket发送一定量数据后停止传输的原因可能是什么?(中断位置大致固定,但并非完全一致)
1. 接收端接收缓冲区溢出
UDP是无连接协议,若接收端未及时读取缓冲区数据,系统接收缓冲区会被填满,后续到达的报文会直接被内核丢弃。发送端持续发送但接收端处理速度跟不上(如单报文处理耗时过长、接收线程阻塞)时,就会出现“接收端停止接收”的现象——实际是后续报文被丢弃,接收端无法感知。
2. 发送端SO_SNDBUF设置未实际生效
你设置了100MB的SO_SNDBUF,但系统存在缓冲区大小上限,实际生效值可能远低于设定值。当发送速度超过内核缓冲区处理能力时,sendto返回成功仅代表数据进入用户态缓冲区,未真正发往网络。若缓冲区被填满,阻塞模式下sendto会阻塞,非阻塞模式下会返回错误,但你的代码未检查sendto返回值,可能掩盖了问题。
建议:添加返回值校验,确认实际发送字节数与报文长度一致:
sent_len = self.server_socket.sendto(message, self.addr) if sent_len != len(message): logger.warning("Partial send: sent %d bytes out of %d", sent_len, len(message))
3. 网络层面的流量管控或分片丢包
- 中间设备限速:路由器、交换机等可能对UDP流量有速率限制,短时间发送大量报文会触发丢包逻辑。即便设置了
MESSAGE_DELAY,若延迟过小仍可能触发管控。 - UDP报文过大导致分片丢包:若
BUFFER_SIZE设置过大,单个UDP报文超过MTU(通常1500字节,减去IP/UDP头后约1472字节)会被分片传输。一旦某分片丢失,整个报文无法重组,接收端无法识别。若接收端依赖序列号排序,会卡在丢失报文的位置,表现为停止接收。
4. 接收端序列号处理逻辑异常
协议依赖序列号重组数据,若接收端处理某序列号报文时出现解析错误、崩溃或死循环,会导致后续报文无法被正确处理或读取,看起来像是停止接收。
5. SO_LINGER选项的副作用
开启SO_LINGER并设置超时,会在socket关闭时等待未发送数据完成,但该选项可能改变内核缓冲区处理逻辑,间接影响发送过程。可暂时移除该选项,验证问题是否消失。
内容的提问来源于stack exchange,提问作者404usernamenotfound

