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



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

使用Ruby的Timeout模块不是一种选择,因为它需要在每次超时时生成和连接线程,这可能会变得非常昂贵。在需要较低套接字超时并且具有大量线程的应用程序中,这基本上会降低性能。这已经在许多地方被注意到,包括堆栈溢出。

我在这里阅读了Mike Perham关于这个主题的精彩文章,为了将问题减少到一个可运行代码文件,我创建了一个TCP服务器的简单示例,该服务器将接收请求,等待请求中发送的时间,然后关闭连接。

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

客户端应该在1秒后超时,但在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("5n")
  # 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

设置这个套接字选项真的适用于任何人吗?

您可以使用Ruby的IO类中的select高效地执行此操作。

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

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

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

  • 准备阅读
  • 准备写作
  • 具有挂起的异常

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

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

因此select的返回值将如下所示:

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

但是,如果给定了可选的超时值,并且在超时秒内没有IO对象准备就绪,select将返回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中具有多次超时的多线程应用程序更快。

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

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

strace还显示了阻止程序的内容。这是我在服务器超时前在屏幕上看到的一行:

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

这意味着程序正在阻止对ppoll的调用,而不是对recvfrom的调用。列出套接字选项的手册页(套接字(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 = "" * 1024
count = LibC.read(sock.fileno, buf, 1024)
if count == -1
  puts 'Timeout'
end

这个代码在这里工作。当然:这是一个丑陋的解决方案,在许多平台上都不起作用,等等。不过这可能是一条出路。

另外请注意,这是我第一次在Ruby中做类似的事情,所以我没有意识到我可能忽略的所有陷阱——特别是,我怀疑我在'long read(int, void *, long)'中指定的类型,以及我传递缓冲区进行读取的方式。

根据我的测试和Jesse Storimer关于"使用TCP套接字"(Ruby)的优秀电子书,超时套接字选项在Ruby 1.9中不起作用(我认为是2.0和2.1)。Jesse说:

您的操作系统还提供本机套接字超时,可以通过SNDTIMEO和RCVTIMEO插座选项。但是,从Ruby 1.9开始,这个特性就不再是功能的"

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

相关内容

  • 没有找到相关文章

最新更新