有没有办法取消挂起的操作(不断开连接)或为 boost 库函数设置超时?
即我想在 boost asio 中设置阻塞套接字的超时时间?
socket.read_some(boost::asio::buffer(pData, maxSize), error_);
示例:我想从套接字读取一些内容,但是如果 10 秒过去了,我想抛出一个错误。
有没有办法取消挂起的操作(不断开连接)或为 boost 库函数设置超时?
即我想在 boost asio 中设置阻塞套接字的超时时间?
socket.read_some(boost::asio::buffer(pData, maxSize), error_);
示例:我想从套接字读取一些内容,但是如果 10 秒过去了,我想抛出一个错误。
TL;博士
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 个线程后开始逐渐减少,因为每个堆分配/释放都将使用内存屏障。
当被问到这个问题时,我猜 ASIO 没有任何关于如何完成 OP 所需的示例,即超时阻塞操作,例如阻塞套接字操作。现在有一些例子可以准确地向您展示如何做到这一点。这个例子看起来很长,但那是因为它的评论很好。它展示了如何在“一次性”模式下使用 ioservice。
我认为这个例子是一个很好的解决方案。这里的其他解决方案破坏了可移植性,并且没有利用 ioservice。如果可移植性不重要并且 ioservice 似乎开销很大——那么——你不应该使用 ASIO。无论如何,您都将创建一个 ioservice(几乎所有 ASIO 功能都依赖于它,甚至是同步套接字),因此请利用它。
ASIO 文档已更新,因此请查看有关如何克服 ASIO 使用的一些“陷阱”的新示例。
您可以执行 async_read 并为您想要的超时设置一个计时器。然后,如果计时器触发,请在您的套接字对象上调用取消。否则,如果发生读取,您可以取消计时器。这当然需要您使用 io_service 对象。
编辑:为您找到了执行此操作的代码片段
在 Linux/BSD 下,操作系统直接支持套接字上的 I/O 操作超时。该选项可以通过启用setsocktopt()
。我不知道是否boost::asio
提供了设置它的方法或公开套接字脚本以允许您直接设置它——后一种情况并不是真正可移植的。
为了完整起见,这是手册页中的描述:
SO_RCVTIMEO和SO_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.
我有同样的问题,经过一番研究,我能想出的最简单、最干净的解决方案是获取底层的本机套接字,然后进行选择,直到有数据要读取。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);
...
也许这对您的情况有用。
继续 grepsedawk 提到的内容。在 asio doco的Timeouts部分下,有几个示例显示了如何在一段时间后取消长时间运行的异步操作。提升 Asio 示例 。异步 TCP 客户端对我帮助最大。
快乐的异步:)
即使在最初的问题之后多年,仍然没有令人满意的答案。
手动使用 select 不是一个好的选择
调用io_service.run_one()
也是一个坏主意,因为可能还有其他异步选项需要 io_service 到 always 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
-- 更新 -- 示例链接已更新。
SO_RCVTIMEO
并SO_SNDTIMEO
从timeval
struct"sys/time.h"
而不是int
. 所以@Pavel Verevkin 的选项 1. 需要采用 atimeval
而不是 anint
和选项 2. 需要实现一个类,因为boost::asio::detail::socket_option::integer
只存储一个整数值。
在 *nix 上,您将使用 alarm(),因此您的套接字调用将因 EINTR 而失败