Linux上的套接字ReceiveTimeout

时间:2014-09-14 07:43:37

标签: linux sockets boost-asio

我正在写一个同步客户端。部分是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;
    }
}

问题在于,即使我指定1s超时,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上运行正常。

1 个答案:

答案 0 :(得分:0)

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中提供的模式。
  • 直接调用OS调用。但是,要小心,因为其他操作可能会间接影响这种方法。例如,如果在套接字上启动异步操作,则套接字将设置为非阻塞。这将导致recv()函数立即在非阻塞套接字上返回,而不管SO_RCVTIMEO套接字选项。