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 调用,但系统差异和错误处理需要由应用负责。
备注:内容仅供参考。
