PAYATHON 2026

Boost.Asio UDP socket 的 async_wait 能否配合 recvfrom() 使用?

数据小陈

结论

可以这样做:对 UDP socket 调用 async_wait(udp::socket::wait_read, handler),等到 socket 可读后,通过 socket.native_handle() 调用系统的 recvfrom()。处理完数据后,再次发起等待。async_wait 只通知 socket 已经可读,不会接收数据。Boost.Asio 文档也展示了通过 native_non_blocking() 和 async_wait() 将系统调用封装为异步操作的方式。async_wait 参考 · native_non_blocking 参考

POSIX 系统上的基本写法

下面的示例展示了基本流程,适用于提供 POSIX recvfrom()、errno 和 socklen_t 的系统。示例假设 socket 已打开并绑定,io_context 正在运行。异步操作期间,缓冲区和 socket 都必须保持有效。

#include <boost/asio.hpp>
#include <array>
#include <cerrno>
#include <sys/socket.h>

namespace asio = boost::asio;
using udp = asio::ip::udp;

std::array<char, 65536> buffer;

// 让直接调用 native_handle() 的系统调用使用非阻塞模式。
socket.native_non_blocking(true);

auto wait_for_data = std::make_shared<std::function<void()>>();
*wait_for_data = [&socket, &buffer, wait_for_data]() {
    socket.async_wait(udp::socket::wait_read,
        [&socket, &buffer, wait_for_data](const boost::system::error_code& ec) {
            if (ec) {
                return; // 处理关闭、取消或其他等待错误
            }

            for (;;) {
                sockaddr_storage peer{};
                socklen_t peer_len = sizeof(peer);
                const auto n = ::recvfrom(
                    socket.native_handle(), buffer.data(), buffer.size(), 0,
                    reinterpret_cast<sockaddr*>(&peer), &peer_len);

                if (n >= 0) {
                    // 处理一个 UDP 数据报;n == 0 也可能是合法的空数据报。
                    continue;
                }

                if (errno == EINTR) {
                    continue;
                }
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    break; // 当前队列已读空
                }

                // 处理其他系统错误。
                return;
            }

            (*wait_for_data)(); // 重新等待下一批数据
        });
};

(*wait_for_data)();

示例使用了 POSIX 类型和错误码。Windows 上需要改用相应的 Winsock 类型和错误处理。实际程序还需要补上数据处理、错误上报和对象生命周期管理。

使用时要注意

  • 设置 native 非阻塞模式,并检查调用是否成功。可读通知发出后,其他读取者可能先取走数据;也可能有多个线程同时处理同一个 socket。因此,收到通知后,recvfrom() 仍可能返回 EAGAIN 或 EWOULDBLOCK。使用非阻塞调用并处理这些结果,可以避免事件循环线程意外阻塞。Boost.Asio 的 native_non_blocking() 用于获取或设置底层 socket 的非阻塞状态。
  • 让同一个组件负责读取。不要在同一个 socket 上同时发起 async_receive_from(),又从其他代码调用 recvfrom(),否则两边会争用同一队列中的数据报。如果有多个线程运行 io_context,也要避免多个回调并发读取这个 socket。
  • 持续读取,直到暂时没有数据,再重新等待。在非阻塞模式下,反复读取到 EAGAIN 或 EWOULDBLOCK 后,再注册下一次 async_wait()。遇到 EINTR 时,通常应重试系统调用。
  • 注意 UDP 数据报边界。每次 recvfrom() 接收一个数据报。缓冲区太小可能会导致数据报被截断;应根据应用允许的最大数据报长度设置缓冲区,并通过平台接口检查是否发生截断。
  • 也可以通过 native handle 写入。发送 UDP 数据时可调用对应平台的 sendto(),但非阻塞错误、地址格式和系统调用差异都需要自行处理。

是否适合用 SSL_read()?

普通 UDP socket 应使用 recvfrom() 或 Asio 的 UDP 接收操作来读取数据。SSL_read() 用于从 OpenSSL 的 SSL 会话中读取 TLS 数据,不能替代普通 UDP socket 的读取函数。如果实际使用的是 DTLS,应调用相应的 OpenSSL DTLS 接口,并按 DTLS 会话方式管理数据报。

通常更简单的选择

如果没有必须调用 native API 的原因,直接使用 async_receive_from() 会更省心。Asio 会负责等待和接收,也更容易保持跨平台行为。如果需要自行调用系统函数,可以使用 async_wait() 配合非阻塞 native 调用,但系统差异和错误处理需要由应用负责。

备注:内容仅供参考。