【问题标题】:Socket ReceiveTimeout on LinuxLinux 上的套接字接收超时
【发布时间】:2014-09-14 07:43:36
【问题描述】:

我正在编写一个同步客户端。其中一部分是一个Connection 对象,它负责实际发送和接收数据。整个库是使用 Boost ASIO ip::tcp::socket 类编写的。

我有一个测试,其中客户端调用服务器上的一个方法(休眠 2 秒),超时时间为 1 秒。我的代码检测到执行花费的时间超过了请求的时间,但它没有及时返回。相反,它在整整 2 秒后返回。

我已将问题缩小到receive 方法:

void Connection::receive(const mutable_buffers_1& buffers, const DurationType& timeout)
{
    // to make sure it isn't 0 by mistake
    auto actualTimeout = std::max(DurationType(milliseconds(1)), timeout);
    SocketReceiveTimeoutOption timeoutOption(actualTimeout);
    error_code ec;
    _socket.set_option(timeoutOption, ec);
    RPC_LOG(TRACE) << "Setting timeout " << actualTimeout << " returned: " << ec.message();
    RPC_LOG(TRACE) << "Receiving...";
    if (_socket.receive(buffers, MSG_WAITALL, ec) != buffer_size(buffers))
    {
        throw RpcCommunicationError("Did not receive the expected number of bytes from connection");
    }
    RPC_LOG(TRACE) << "Received! With error code: " << ec.message();
}

DurationType 只是一个方便的 typedef:

typedef boost::chrono::system_clock ClockType;
typedef ClockType::time_point::duration DurationType;

SocketReceiveTimeoutOption 是为套接字实现的选项:

template <int Name>
class SocketTimeoutOption
{
public:
    #ifdef BSII_WINDOWS
    SocketTimeoutOption(const DurationType& timeout) : _value(static_cast<DWORD>(boost::chrono::duration_cast<boost::chrono::milliseconds>(timeout).count())) {}
    #else
    SocketTimeoutOption(const DurationType& timeout) : _value(Utils::toTimeval(timeout)) {}
    #endif

    // Get the level of the socket option.
    template <typename Protocol>
    int level(const Protocol&) const
    {
        return SOL_SOCKET;
    }

    // Get the name of the socket option.
    template <typename Protocol>
    int name(const Protocol&) const
    {
        return Name;
    }

    // Get the address of the timeout data.
    template <typename Protocol>
    void* data(const Protocol&)
    {
        return &_value;
    }

    // Get the address of the timeout data.
    template <typename Protocol>
    const void* data(const Protocol&) const
    {
        return &_value;
    }

    // Get the size of the boolean data.
    template <typename Protocol>
    std::size_t size(const Protocol&) const
    {
        return sizeof(_value);
    }

private:
    #ifdef BSII_WINDOWS
    DWORD _value;
    #else
    timeval _value;
    #endif
};

typedef SocketTimeoutOption<SO_RCVTIMEO> SocketReceiveTimeoutOption;
typedef SocketTimeoutOption<SO_SNDTIMEO> SocketSendTimeoutOption;

最后

namespace Utils
{
    inline
        timeval toTimeval(const DurationType& duration)
    {
            timeval val;
            auto seconds = boost::chrono::duration_cast<boost::chrono::seconds>(duration); // TODO: make sure this is truncated down in case there's fractional seconds
            val.tv_sec = static_cast<long>(seconds.count());

            auto micro = boost::chrono::duration_cast<boost::chrono::microseconds>(duration - seconds);
            val.tv_usec = static_cast<long>(micro.count());

            return val;
    }
}

问题是即使我指定了 1 秒的超时时间,receive 方法仍然需要整个 2 秒。这是日志:

2014-09-14 10:27:53.348383 |跟踪 | 0x007f24e50ae7c0 |设置超时 999917107 纳秒返回:成功
2014-09-14 10:27:53.348422 |跟踪 | 0x007f24e50ae7c0 |正在接收...
2014-09-14 10:27:55.349152 |跟踪 | 0x007f24e50ae7c0 |已收到!带有错误代码:成功

如您所见,设置超时有效,但 receive 方法仍然需要 2 秒。

相同的代码在 Windows 上也能正常工作。

【问题讨论】:

  • DurationTypeSocketReceiveTimeoutOption 未定义。如果它们是实际 boost 类型的 typedef,请在您的示例中显示 typedef 或使用实际的 boost 类型。
  • 谢谢。编辑原始问题。现在我看到在 Linux 案例中设置超时选项时可能存在错误。我会检查并更新。
  • 更新:我认为将持续时间转换为 timeval 是可以的。我添加了一些日志记录并得到以下结果:Converting 999718938 nanoseconds to timeval Got: 0 seconds + 999718 useconds

标签: linux sockets boost-asio


【解决方案1】:

socket::receive() 将阻塞直到:

  • 已成功接收到一个或多个字节的数据
  • 发生错误,导致无法接收数据

对于阻塞同步操作,如果底层操作系统操作返回一个非关键错误,例如指示该操作将阻塞或应该再次尝试,那么 Boost.Asio 将阻塞在 poll() 等待文件描述符准备就绪。对poll() 的阻塞调用不受SO_RCVTIMEO 套接字选项的影响。一旦文件描述符准备就绪,Boost.Asio 将重新尝试该操作。

因此,原问题中的场景如下:

Time | Client                                 | Server
-----+----------------------------------------+-------------------------------
     | socket.connect(...);                   | acceptor.accept(...);
0.00 | socket.set_option(timeout(second(1))); | sleep(seconds(2));
0.01 | socket.receive(...);                   | 
0.02 | |-- recv(...);                         |  
1.02 | | // timeout, errno = EAGAIN           | 
1.03 | |-- poll(socket);                      | 
2.00 | | // data available, poll unblocks     | socket.write(...);
2.01 | `-- recv(...);// success               |

要获得所需的超时行为,可以:

  • 使用官方 Boost.Asio timeout examples 中提供的模式。
  • 直接调用操作系统调用。但是,请谨慎,因为其他操作可能会间接影响此方法。例如,如果在套接字上启动异步操作,那么套接字将被设置为非阻塞。这将导致recv() 函数立即在非阻塞套接字上返回,而不管SO_RCVTIMEO 套接字选项如何。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-08
    • 1970-01-01
    • 2012-04-20
    • 2012-10-18
    • 2010-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多