boost-asio-udp-同步超时陷阱

2024/03/24

Tags: C++ Boost Asio

Table of Contents

2024/03/24 阴, 约了许久没见面的妹妹吃了一个饭,她去找他同学玩了,我到咖啡店找点下周要开发的nvidia jetson xavier的资料, 由于板子没在身边,具体型号也不记得,所以有时间记录一下去年遇到的一个boost asio 同步通讯中,客户端设置超时的坑, 顺便记录一下boost udp 客户端 通讯的example, 同步,异步应该是大同小异, 我使用的boost 版本是1.83

0. boost udp 同步超时陷阱

遇到这个问题时,我在开发一个gige 协议的客户端,用于windows和linux环境,使用gige相机。gige中通过gvcp和gvsp来对相机进行交互和取流,gvcp是udp协议,所以我用的是udp socket。开始时,我在windows上开发, 使用下面这段代码设置超时时间 ,它可以正常的工作 ,当超时发生时,返回超时的结果。

代码中先创建了endpoint和udp socket,然后把它们绑定在一起,然后设置socket的超时时间 为5000毫秒

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void foo()
{
    try {
        boost::asio::io_service io_service;
        boost::asio::ip::udp::socket socket(io_service);

        socket.open(boost::asio::ip::udp::v4());
        boost::asio::ip::udp::endpoint if_endpoint(boost::asio::ip::address::from_string("0.0.0.0"), 0);
        socket.bind(if_endpoint);

        boost::asio::ip::udp::endpoint endpoint(boost::asio::ip::address::from_string(devIp), 3956);

        boost::array<unsigned char,16> hex_data = {/* ... */ };
        socket.send_to(boost::asio::buffer(hex_data), endpoint);
        // set timeout 5000ms
        socket.set_option(boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO>{ 5000 });
        boost::array<char, 600> recv_buffer;
        boost::asio::ip::udp::endpoint remote_endpoint;

        boost::system::error_code error;
        size_t bytes_received = socket.receive_from(boost::asio::buffer(recv_buffer), remote_endpoint, 0, error);
        Q_UNUSED(bytes_received)
        if (!error) {
            // do stuff
            }
        } else {
            std::cerr << "Error receiving message: " << error.message() << std::endl;
        }
    } catch (const boost::system::system_error& e) {
        // timeout 
        if (e.code() == boost::asio::error::timed_out) {
            std::cerr << "Timeout: No response received within 1 second." << std::endl;
        } else {
            std::cerr << "Error receiving message: " << e.what() << std::endl;
        }
    }
    catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return "";
}

但是当我把这段代码放到linux上运行时,当超时发生时,会导致receive_from永远不会返回。

1. window设置超时有效的原因

window和 linux的receive_from在经过平台适配层后,分发到平台系统api,

windows的api在boost socket_ops.ipp中为sync_recvfrom1中,api为WSARecvFrom

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
signed_size_type recvfrom(socket_type s, buf* bufs, size_t count,
    int flags, void* addr, std::size_t* addrlen, boost::system::error_code& ec)
{
#if defined(BOOST_ASIO_WINDOWS) || defined(__CYGWIN__)
  // Receive some data.
  DWORD recv_buf_count = static_cast<DWORD>(count);
  DWORD bytes_transferred = 0;
  DWORD recv_flags = flags;
  int tmp_addrlen = (int)*addrlen;
  int result = ::WSARecvFrom(s, bufs, recv_buf_count, &bytes_transferred, //调用 window api
      &recv_flags, static_cast<socket_addr_type*>(addr), &tmp_addrlen, 0, 0); 
  get_last_error(ec, true);
  *addrlen = (std::size_t)tmp_addrlen;
  if (ec.value() == ERROR_NETNAME_DELETED)
    ec = boost::asio::error::connection_reset;
  else if (ec.value() == ERROR_PORT_UNREACHABLE)
    ec = boost::asio::error::connection_refused;
  else if (ec.value() == WSAEMSGSIZE || ec.value() == ERROR_MORE_DATA)
    result = 0;
  if (result != 0)
    return socket_error_retval;
  boost::asio::error::clear(ec);
  return bytes_transferred;
#else // defined(BOOST_ASIO_WINDOWS) || defined(__CYGWIN__)
  //...省略
#endif // defined(BOOST_ASIO_WINDOWS) || defined(__CYGWIN__) 
}

可以通过设置SO_RCVTIMEO 来使 WSARecvFrom ,recvfrom等阻塞式的读数据的函数超时返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main()
{
    int iSock = socket(AF_INET, SOCK_DGRAM, 0); 
	char szBuf[1024] = {0};
	struct timeval tv;
	tv.tv_sec = 1;
	tv.tv_usec =  0;
	setsockopt(iSock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); 
    int iRet = recvfrom(iSock, szBuf, sizeof(szBuf) - 1, 0, NULL, NULL); 
    printf("iRet is [%d]\n", iRet);
 
    close(iSock);
}

因为我们通过boost的方式设置了SO_RCVTIMEO,

1
 socket.set_option(boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO>{ 5000 });

2. linux设置超时无效的原因

而linux调用系统api的位置在 boost socket_ops.ipp文件中的poll_read中,外部传入的超时时间为常量-1,即无限等待

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int poll_read(socket_type s, state_type state,
    int msec, boost::system::error_code& ec)
{
  if (s == invalid_socket)
  {
    ec = boost::asio::error::bad_descriptor;
    return socket_error_retval;
  }

#if defined(BOOST_ASIO_WINDOWS) //widnows逻辑
 //略
#else      // linux逻辑

  pollfd fds;
  fds.fd = s;
  fds.events = POLLIN;
  fds.revents = 0;
  int timeout = (state & user_set_non_blocking) ? 0 : msec;
  int result = ::poll(&fds, 1, timeout); //timeout实参常数-1
  get_last_error(ec, result < 0);
#endif 
  if (result == 0)
    if (state & user_set_non_blocking)
      ec = boost::asio::error::would_block;
  return result;
}

3. 解法

一种即达到同步等等,windows和linux又不需要维护两个版本的方法,是使用异步接口写同步等待 同步等待不必非得用同步接口,asio 擅长的本身就是异步

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void foo()
{
    std::string result;
    boost::asio::io_service io_service;
    boost::asio::io_service::work work(io_service);
    std::thread thread([&io_service](){ io_service.run(); });

    boost::asio::ip::udp::socket socket(io_service);

    socket.open(boost::asio::ip::udp::v4());
    boost::asio::ip::udp::endpoint if_endpoint(boost::asio::ip::address::from_string("0.0.0.0"), 0);
    socket.bind(if_endpoint);

    boost::asio::ip::udp::endpoint endpoint(boost::asio::ip::address::from_string(devIp), 3956);

    // send datas
    boost::array<unsigned char,16> hex_data = { /* ... */ };

    // send 
    std::future<size_t> bytes_send =  socket.async_send_to(boost::asio::buffer(hex_data), endpoint,boost::asio::use_future);
    boost::array<char, 600> recv_buffer;
    boost::asio::ip::udp::endpoint remote_endpoint;
    bytes_send.get();

    std::future<size_t> bytes_received = socket.async_receive_from(boost::asio::buffer(recv_buffer), remote_endpoint,boost::asio::use_future);

    std::future_status status = bytes_received.wait_for(std::chrono::milliseconds(5000));
    if (std::future_status::timeout == status) {
        socket.cancel();
        // timeout
    } else if (status == std::future_status::ready) {

        //do stuff
    }
    else
    {
        //other errors
    }
}
>> Home

Comments