如何在 boost asio 中设置阻塞套接字的超时时间?

Posted

技术标签:

【中文标题】如何在 boost asio 中设置阻塞套接字的超时时间?【英文标题】:How to set a timeout on blocking sockets in boost asio? 【发布时间】:2010-09-22 10:55:50 【问题描述】:

有没有办法取消挂起的操作(不断开连接)或为 boost 库函数设置超时?

即我想在 boost asio 中设置阻塞套接字的超时时间?

socket.read_some(boost::asio::buffer(pData, maxSize), error_);

示例:我想从套接字中读取一些内容,但是如果 10 秒过去了,我想抛出一个错误。

【问题讨论】:

【参考方案1】:

TL;DR

socket.set_option(boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO> 200 );

完整答案 这个问题多年来一直被反复问。到目前为止我看到的答案很差。我将在此问题的第一次出现中添加此信息。

如果作者只是为所有同步和异步 io 函数添加一个可选参数超时,每个尝试使用 ASIO 来简化他们的网络代码的人都会非常高兴。不幸的是,这不太可能发生(在我看来,只是出于意识形态的原因,毕竟 ASIO 中的 AS 是有原因的)。

所以这些是迄今为止可用的给这只可怜的猫剥皮的方法,它们都不是特别开胃的。假设我们需要 200 毫秒的超时时间。

1) 好(坏)旧的套接字 API:

const int timeout = 200;
::setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof timeout);//SO_SNDTIMEO for send ops

请注意这些特点: - 用于超时的 const int - 在 Windows 上,所需的类型实际上是 DWORD,但幸运的是当前的编译器集具有相同的类型,因此 const int 在 Win 和 Posix 世界中都可以使用。 - (const char*) 表示值。在 Windows 上需要 const char*,Posix 需要 const void*,在 C++ 中 const char* 会默默地转换为 const void*,而反之则不然。

优点:可以工作并且可能会一直工作,因为套接字 API 是旧的和稳定的。很简单。快速地。 缺点:从技术上讲,setsockopt 和宏可能需要适当的头文件(在 Win 上不同,甚至在不同的 UNIX 风格上),但 ASIO 的当前实现无论如何都会污染全局命名空间。需要一个用于超时的变量。不是类型安全的。在 Windows 上,要求套接字处于重叠模式才能工作(目前的 ASIO 实现幸运地使用了这种模式,但它仍然是一个实现细节)。丑!

2) 自定义 ASIO 套接字选项:

typedef boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO> rcv_timeout_option; //somewhere in your headers to be used everywhere you need it
//...
socket.set_option(rcv_timeout_option 200 );

优点:足够简单。快速地。漂亮(使用 typedef)。 缺点:取决于 ASIO 实现细节,这可能会改变(但 OTOH 最终一切都会改变,而且这样的细节不太可能改变,然后是受标准化的公共 API)。但如果发生这种情况,您将不得不根据https://www.boost.org/doc/libs/1_68_0/doc/html/boost_asio/reference/SettableSocketOption.html 编写一个类(这当然是一个主要的 PITA,这要归功于 ASIO 的这部分明显的过度设计),或者最好还是恢复为 1。

3) 使用 C++ 异步/未来工具。

#include <future>
#include <chrono>
//...
auto status = std::async(std::launch::async, [&] () /*your stream ops*/ )
    .wait_for(std::chrono::milliseconds 200 );
switch (status)
    
    case std::future_status::deferred:
    //... should never happen with std::launch::async
        break;
    case std::future_status::ready:
    //...
        break;
    case std::future_status::timeout:
    //...
        break;
    

优点:标准。 缺点:总是启动一个新线程(在实践中),相对较慢(可能对客户端来说已经足够了,但会导致服务器的 DoS 漏洞,因为线程和套接字是“昂贵的”资源)。不要尝试使用 std::launch::deferred 而不是 std::launch::async 来避免新线程启动,因为 wait_for 将始终返回 future_status::deferred 而不会尝试运行代码。

4) ASIO 规定的方法 - 仅使用异步操作(这并不是问题的真正答案)。

优点:如果不需要短事务的巨大可扩展性,对服务器也足够好。 缺点:相当罗嗦(所以我什至不会包含示例 - 请参阅 ASIO 示例)。需要对异步操作及其完成处理程序使用的所有对象进行非常仔细的生命周期管理,这实际上要求在异步操作中包含和使用此类数据的所有类都派生自 enable_shared_from_this,这需要在堆上分配所有此类类,这意味着(至少对于短操作而言)可伸缩性将在大约 16 个线程后开始逐渐减少,因为每个堆分配/释放都将使用内存屏障。

【讨论】:

我得到一个无效的参数(使用 c++98 提升 1.55):int recvTimeoutInMS = 1000; const boost::asio::detail::socket_option::integer&lt;SOL_SOCKET, SO_RCVTIMEO&gt; option = boost::asio::detail::socket_option::integer&lt;SOL_SOCKET, SO_RCVTIMEO&gt;(recvTimeoutInMS); socket_.set_option(option); 我认为您需要在第三点进行更正——当然,我们绝对可以使用 std::future 作为解决方法,但它并不总是需要 async(特别是在 boost- asio),您可以通过使用use_future模型启动异步操作来获得std::future:不涉及线程。 @Pavel Verevkin @Explorer_N 再次阅读答案并不能帮助我将代码转换为 c++98 与C++版本无关。 SO_RCVTIMEOSO_SNDTIMEO 在 POSIX 系统上需要 struct timeval 参数,而不是 int。事实上,int 只能在 Win32 上正常工作。【参考方案2】:

当被问到这个问题时,我猜 ASIO 没有任何关于如何完成 OP 所需的示例,即超时阻塞操作,例如阻塞套接字操作。现在有一些例子可以准确地向您展示如何做到这一点。这个例子看起来很长,但那是因为它的评论很好。它展示了如何在“一次性”模式下使用 ioservice。

我认为这个例子是一个很好的解决方案。这里的其他解决方案破坏了可移植性,并且没有利用 ioservice。如果可移植性不重要并且 ioservice 似乎开销很大——那么——你不应该使用 ASIO。无论如何,您都将创建一个 ioservice(几乎所有 ASIO 功能都依赖于它,甚至是同步套接字),因此,请利用它。

Timeout a blocking asio tcp operation

Timeout a blocking asio udp operation

ASIO 文档已更新,因此请查看有关如何克服 ASIO 使用的一些“陷阱”的新示例。

【讨论】:

在 Windows 上,io_service.run_one() 从不阻塞 async_read(至少在 Boost 1.59.0 中),从而导致 100% 的 CPU 使用率。 示例很长,因为文档是冗余的 3 倍。我不认为“评论很好”。【参考方案3】:

您可以执行 async_read 并为您想要的超时设置一个计时器。然后,如果计时器触发,请在您的套接字对象上调用取消。否则,如果发生读取,您可以取消计时器。这当然需要您使用 io_service 对象。

编辑:为你找到了一个代码 sn-p 来做这个

http://lists.boost.org/Archives/boost/2007/04/120339.php

【讨论】:

此 sn-p 包含对 io_service::reset() 的调用。但它的文档说This function must not be called while there are any unfinished calls to the run(), run_one(), poll() or poll_one() functions. 如果这段代码要在 asio 异步回调事件中运行,也就是说,在 io_service::run 调用的东西中,我怀疑你会得到未定义的行为。【参考方案4】:

在 Linux/BSD 下,操作系统直接支持套接字上的 I/O 操作超时。该选项可以通过setsocktopt() 启用。我不知道boost::asio 是否提供了设置它的方法或公开了套接字脚本以允许您直接设置它——后一种情况并不是真正可移植的。

为了完整起见,这里是手册页中的描述:

SO_RCVTIMEOSO_SNDTIMEO

          Specify the receiving or sending  timeouts  until  reporting  an
          error.  The argument is a struct timeval.  If an input or output
          function blocks for this period of time, and data has been  sent
          or  received,  the  return  value  of  that function will be the
          amount of data transferred; if no data has been transferred  and
          the  timeout has been reached then -1 is returned with errno set
          to EAGAIN or EWOULDBLOCK just as if the socket was specified  to
          be  non-blocking.   If  the timeout is set to zero (the default)
          then the operation  will  never  timeout.   Timeouts  only  have
          effect  for system calls that perform socket I/O (e.g., read(2),
          recvmsg(2), send(2), sendmsg(2)); timeouts have  no  effect  for
          select(2), poll(2), epoll_wait(2), etc.

【讨论】:

这将是一个很好的解决方案,但他们没有这样的套接字选项。在此处查看套接字选项:boost.org/doc/libs/1_37_0/doc/html/boost_asio/reference.html 但是据我所知,如果 asio 的 read_some() 没有读取任何内容,它仍然会在内部继续永远循环,从而取消您可能在本机套接字上设置的 SO_RCVTIMEO 的效果。看起来使用 select() 和 socket.native() 仍然是最明显的超时方式。 @Stéphane 这是唯一可能的解决方案吗?如果是,你有例子吗? @leon22 那是 5 年前的事了!我对 ASIO 的使用来自旧版本的 Boost。我希望到现在 ASIO 已经被修复以允许超时——他们还没有修复这个问题吗?至于示例...在 ASIO 套接字上调用 select() 很简单。 int rc = select(s.native() + 1, &amp;fds, NULL, NULL, &amp;timeout);【参考方案5】:

我也有同样的问题,经过一番研究,我能想到的最简单、最干净的解决方案是获取底层的本机套接字,然后进行选择,直到有数据要读取。 Select 将采用超时参数。当然,使用本机套接字开始违背使用 asio 的观点,但同样,这似乎是最干净的方式。据我所知,asio 没有提供一种轻松实现同步使用的方法。代码:

        // socket here is:  boost::shared_ptr<boost::asio::ip::tcp::socket> a_socket_ptr

        // Set up a timed select call, so we can handle timeout cases.

        fd_set fileDescriptorSet;
        struct timeval timeStruct;

        // set the timeout to 30 seconds
        timeStruct.tv_sec = 30;
        timeStruct.tv_usec = 0;
        FD_ZERO(&fileDescriptorSet);

        // We'll need to get the underlying native socket for this select call, in order
        // to add a simple timeout on the read:

        int nativeSocket = a_socket_ptr->native();

        FD_SET(nativeSocket,&fileDescriptorSet);

        select(nativeSocket+1,&fileDescriptorSet,NULL,NULL,&timeStruct);

        if(!FD_ISSET(nativeSocket,&fileDescriptorSet)) // timeout

                std::string sMsg("TIMEOUT on read client data. Client IP: ");

                sMsg.append(a_socket_ptr->remote_endpoint().address().to_string());

                throw MyException(sMsg);
        

        // now we know there's something to read, so read
        boost::system::error_code error;
        size_t iBytesRead = a_socket_ptr->read_some(boost::asio::buffer(myVector), error);

        ...

也许这对你的情况有用。

【讨论】:

【参考方案6】:

继续 grepsedawk 提到的内容。在 asio doco 的 Timeouts 部分下,有几个示例展示了如何在一段时间后取消长时间运行的异步操作。 Boost Asio Examples . Async TCP client helped me the most.

愉快的异步:)

【讨论】:

【参考方案7】:

即使在最初的问题之后多年,仍然没有令人满意的答案。

手动使用选择不是一个好的选择

    文件描述符数必须小于 1024 由于校验和错误,FD 可能被错误地报告为准备就绪。

调用io_service.run_one() 也是一个坏主意,因为可能有其他异步选项需要 io_service 始终run()。而且boost关于阻塞tcp客户端的文档很难理解。

所以这是我的解决方案。关键思想如下:


    Semaphore r_sem;
    boost::system::error_code r_ec;
    boost::asio::async_read(s,buffer,
                            [this, &r_ec, &r_sem](const boost::system::error_code& ec_, size_t) 
                                r_ec=ec_;
                                r_sem.notify();
                            );
    if(!r_sem.wait_for(std::chrono::seconds(3))) // wait for 3 seconds
    
        s.cancel();
        r_sem.wait();
        throw boost::system::system_error(boost::asio::error::try_again);
    
    else if(r_ec)
        throw boost::system::system_error(r_ec);

这里Semaphore只是一个互斥锁和一个condition_variable。wait_for是由http://en.cppreference.com/w/cpp/thread/condition_variable/wait_for实现的

完整代码在https://github.com/scinart/cpplib/blob/master/include/asio.hpp 示例:https://github.com/scinart/cpplib/blob/6e9a1690bf68971b809be34dfe432949d9a9f727/standalone_example/boost_block_tcp_client_server.cpp

--更新-- 示例链接已更新。

【讨论】:

【参考方案8】:

SO_RCVTIMEOSO_SNDTIMEO"sys/time.h" 接收 timeval 结构,而不是 int。所以@Pavel Verevkin 的选项 1. 需要采用 timeval 而不是 int 和选项 2. 需要实现一个类,因为 boost::asio::detail::socket_option::integer 只存储一个整数值。

【讨论】:

【参考方案9】:

在 *nix 上,您将使用 alarm(),因此您的套接字调用将因 EINTR 而失败

【讨论】:

难道不是所有线程中的所有套接字调用都因 EINTR 而失败?听起来很糟糕。

以上是关于如何在 boost asio 中设置阻塞套接字的超时时间?的主要内容,如果未能解决你的问题,请参考以下文章

boost::asio 扩展 TCP 套接字

如何接受boost :: asio :: ssl :: stream 作为boost :: asio :: ip :: tcp :: socket类型的参数

如何在 Boost.ASIO 中分配已连接的本机套接字类型 (TCP)

如何确定是不是有数据可从 boost::asio 中的套接字读取?

C++ Boost.Asio - tcp 套接字异步写入

使用 boost asio udp 套接字时如何设置本地端点