20

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

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

我在这里阅读了 Mike Perham 关于该主题的出色文章,为了将问题减少到一个可运行代码文件,我创建了一个 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

设置此套接字选项实际上对任何人都有效吗?

4

3 回答 3

25

您可以使用Ruby 的 IO 类有效地做到这一点。select

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

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

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

  • 准备阅读
  • 准备写作
  • 有待处理的异常

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

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

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

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

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

因此,如果您想在 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

This has the benefit of not spinning up a new thread for each timeout (as in the Timeout module) and will make multi-threaded applications with many timeouts much faster in Ruby.

于 2012-08-24T14:14:05.473 回答
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 中做类似的事情,所以我不知道我可能忽略的所有陷阱——特别是,我怀疑我指定的类型'long read(int, void *, long)'和我的方式'正在传递一个要读取的缓冲区。

于 2012-03-24T18:36:08.647 回答
6

Based on my testing, and Jesse Storimer's excellent ebook on "Working with TCP Sockets" (in Ruby), the timeout socket options do not work in Ruby 1.9 (and, I presume 2.0 and 2.1). Jesse says:

Your operating system also offers native socket timeouts that can be set via the SNDTIMEO and RCVTIMEO socket options. But, as of Ruby 1.9, this feature is no longer functional."

Wow. I think the moral of the story is to forget about these options and use IO.select or Tony Arcieri's NIO library.

于 2013-10-11T19:57:20.967 回答