如何获取socket上最新到达的UDP数据包并丢弃历史旧数据包
UDP接收端仅取最新数据包的高效实现
针对无人机传感器这类周期性上报、旧数据完全无价值的UDP场景,有两个比循环调用recv(MSG_DONTWAIT)开销低得多的实现方案,不需要反复做用户态/内核态切换:
方案1:零额外开销内核自动丢旧包(Linux平台首选)
直接通过setsockopt把socket的接收缓冲区大小设置为单包最大长度的2倍左右即可,示例代码:// 这里MAX_PKT_LEN替换为你实际业务里无人机发送的最大UDP包长度 int rcv_buf_size = 2 * MAX_PKT_LEN; setsockopt(sock_fd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(rcv_buf_size));原理非常简单:Linux内核UDP接收队列满了之后,会自动丢弃队列中最早到达的旧数据包,给新到的包腾位置。设成2倍单包长度的话,队列里最多只会存1个完整的待读包+刚到的新包,你每次调用读接口拿到的永远是最新的数据包,完全不需要用户态写任何丢包逻辑,没有任何额外开销。我自己做同类型工业传感器UDP采集的时候用这个方案跑了3年多,稳定性和实时性都比用户态丢包的方案好很多。
注意不要把缓冲区设成刚好1倍单包长度,留1倍冗余避免包大小波动导致新到的包因为剩余空间不足被意外丢弃。方案2:批量读队列取最后一包(跨平台通用)
如果不想修改socket缓冲区配置,或者需要兼容Windows等其他系统,就不要循环调用单包recv,改用批量接收接口一次清空队列:- Linux下用
recvmmsg,一次系统调用就能把队列里攒的所有数据包全部读出来,直接取最后一包就是最新的,前面的包直接丢弃即可,示例代码片段:#define BATCH_READ_NUM 64 struct mmsghdr msg_vec[BATCH_READ_NUM]; struct iovec io_vec[BATCH_READ_NUM]; char pkt_buf[BATCH_READ_NUM][MAX_PKT_LEN]; // 初始化iovec和mmsghdr结构,给每个包绑定对应缓冲区 for (int i = 0; i < BATCH_READ_NUM; i++) { io_vec[i].iov_base = pkt_buf[i]; io_vec[i].iov_len = MAX_PKT_LEN; msg_vec[i].msg_hdr.msg_iov = &io_vec[i]; msg_vec[i].msg_hdr.msg_iovlen = 1; } int pkt_cnt = recvmmsg(sock_fd, msg_vec, BATCH_READ_NUM, MSG_DONTWAIT, NULL); if (pkt_cnt > 0) { // 数组最后一个有效元素就是最新到达的包,前面的全部是过期数据直接丢弃 process_sensor_data(pkt_buf[pkt_cnt - 1], msg_vec[pkt_cnt - 1].msg_len); } - Windows平台同理,用
WSARecv投递多个接收缓冲做批量读取,逻辑完全一致。
这个方案哪怕队列里攒了几十上百个包,也只需要触发一次系统调用,比循环调用单包recv的切换开销低一个数量级以上,不会出现高负载下读包速度赶不上来包速度、永远拿不到最新包的问题。
- Linux下用
避坑提示:不要用循环
recv(MSG_DONTWAIT)读空队列的实现,包量稍大的时候频繁的系统调用切换会吃掉大量CPU,而且如果来包速度比你单包读取的速度快,你会永远在读旧包,最新的传感器数据反而拿不到。
内容的提问来源于stack exchange,提问作者M D
相关产品推荐
相关产品推荐

