通过SO_RCVTIMEO套接字选项在Ruby中设置套接字超时

时间:2012-03-24 16:49:19

标签: ruby sockets io timeout tcp-ip

我正在尝试通过SO_RCVTIMEO套接字选项在Ruby中使套接字超时,但它似乎对任何最近的* nix操作系统没有影响。

使用Ruby的Timeout模块不是一个选项,因为它需要为每个超时生成并加入线程,这可能会变得很昂贵。在需要低套接字超时且具有大量线程的应用程序中,它基本上会导致性能下降。许多地方都注意到这一点,包括Stack Overflow

我已经阅读了Mike Perham关于主题here的优秀帖子,并努力将问题减少到一个可运行代码文件,创建了一个接收请求的TCP服务器的简单示例,等待金额在请求中发送的时间然后关闭连接。

客户端创建套接字,将接收超时设置为1秒,然后连接到服务器。客户端告诉服务器在5秒后关闭会话,然后等待数据。

客户端应在一秒钟后超时,但在5之后成功关闭连接。

#!/usr/bin/env ruby
require 'socket'

def timeout
  sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)

  # Timeout set to 1 second
  timeval = [1, 0].pack("l_2")
  sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval

  # Connect and tell the server to wait 5 seconds
  sock.connect(Socket.pack_sockaddr_in(1234, '127.0.0.1'))
  sock.write("5\n")

  # Wait for data to be sent back
  begin
    result = sock.recvfrom(1024)
    puts "session closed"
  rescue Errno::EAGAIN
    puts "timed out!"
  end
end

Thread.new do
  server = TCPServer.new(nil, 1234)
  while (session = server.accept)
    request = session.gets
    sleep request.to_i
    session.close
  end
end

timeout

我也试过用TCPSocket做同样的事情(它会自动连接),并在redis和其他项目中看到类似的代码。

此外,我可以通过调用getsockopt来验证是否已设置该选项:

sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO).inspect

设置此套接字选项是否真的适用于任何人?

3 个答案:

答案 0 :(得分:24)

您可以使用Ruby的IO类中的select 高效

IO::select需要4个参数。前三个是要监视的套接字数组,最后一个是超时(以秒为单位指定)。

select的工作方式是通过阻塞使IO对象的列表为给定的操作做好准备,直到它们中的至少一个准备好被读取,写入或想要引发错误。

因此,前三个参数对应于要监视的不同类型的状态。

  • 准备阅读
  • 准备写作
  • 有待处理的例外

第四个是您要设置的超时(如果有)。我们将利用这个参数。

Select返回一个数组,该数组包含IO对象数组(本例中为套接字),操作系统认为这些数组对于正在监视的特定操作已准备就绪。

所以select的返回值如下所示:

[
  [sockets ready for reading],
  [sockets ready for writing],
  [sockets raising errors]
]

但是,如果给出了可选的超时值并且在超时秒内没有准备好IO对象,则选择返回nil

因此,如果您想在Ruby中执行高性能IO超时并避免使用Timeout模块,则可以执行以下操作:

让我们构建一个示例,我们等待timeout秒,以便socket读取:

ready = IO.select([socket], nil, nil, timeout)

if ready
  # do the read
else
  # raise something that indicates a timeout
end

这样做的好处是不会为每个超时启动一个新线程(如Timeout模块中),并且会在Ruby中使多线程应用程序的执行速度更快更多

答案 1 :(得分:6)

我认为你基本上没有运气。当我使用strace运行您的示例时(仅使用外部服务器来保持输出清洁),很容易检查setsockopt是否确实被调用:

$ strace -f ruby foo.rb 2>&1 | grep setsockopt
[pid  5833] setsockopt(5, SOL_SOCKET, SO_RCVTIMEO, "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16) = 0

strace还会显示阻止该计划的内容。这是我在服务器超时之前在屏幕上看到的行:

[pid  5958] ppoll([{fd=5, events=POLLIN}], 1, NULL, NULL, 8

这意味着该计划阻止此次ppoll来电,而不是拨打recvfrom。列出套接字选项(socket(7))的手册页说明:

  

超时对select(2),poll(2),epoll_wait(2)等没有影响。

所以超时正在设置但没有效果。我希望我在这里错了,但似乎没有办法在Ruby中改变这种行为。我快速浏览了一下实现,但没有找到明显的出路。再说一次,我希望我错了 - 这似乎是基本的,怎么会不存在呢?

一种(非常难看的)解决方法是使用dl直接呼叫readrecvfrom。这些调用受您设置的超时影响。例如:

require 'socket'
require 'dl'
require 'dl/import'

module LibC
  extend DL::Importer
  dlload 'libc.so.6'
  extern 'long read(int, void *, long)'
end

sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
timeval = [3, 0].pack("l_l_")
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval
sock.connect( Socket.pack_sockaddr_in(1234, '127.0.0.1'))

buf = "\0" * 1024
count = LibC.read(sock.fileno, buf, 1024)
if count == -1
  puts 'Timeout'
end

此代码适用于此处。当然:这是一个丑陋的解决方案,它不适用于许多平台,等等。它可能是一个出路。

另外请注意,这是我第一次在Ruby中做类似的事情,所以我不知道我可能忽略的所有陷阱 - 特别是,我怀疑我在{{中指定的类型1}}以及我传递缓冲区以便阅读的方式。

答案 2 :(得分:6)

基于我的测试,以及Jesse Storimer关于“使用TCP套接字”的优秀电子书(在Ruby中),Ruby 1.9中的超时套接字选项不起作用(我认为2.0和2.1) )。杰西说:

  

您的操作系统还提供可通过以下设置的本机套接字超时   SNDTIMEO和RCVTIMEO套接字选项。但是,从Ruby 1.9开始,此功能不再存在   功能“。

哇。我认为故事的寓意是忘记这些选项并使用IO.select或Tony Arcieri的NIO库。