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

Posted

技术标签:

【中文标题】通过 SO_RCVTIMEO 套接字选项在 Ruby 中设置套接字超时【英文标题】:Set socket timeout in Ruby via SO_RCVTIMEO socket option 【发布时间】:2012-04-08 20:35:39 【问题描述】:

我正在尝试通过 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

设置这个套接字选项真的对任何人都有效吗?

【问题讨论】:

这种问题之前已经发过,看来最好的答案是在recv调用周围使用Ruby的timeout库。 不,使用超时库会产生线程,因此每次超时都需要创建和销毁一个线程,这可能会变得非常昂贵。我将更新我的答案以反映它效率不高。 七年后,我们也了解到,它不仅效率低,而且危险。 【参考方案1】:

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

IO::select 有 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 中大大更快。

【讨论】:

假设读取应该是 readpartial 或 read_nonblock 我知道此时这是一个旧线程,但是这如何处理似乎发生在 SSL .connect() 上的超时? 给我发电子邮件,我很乐意提供帮助。 任何人都可以向我确认,这可以在使用 apache/passenger 的 rails 应用程序的控制器方法中使用 在 Ruby 中使用套接字选项 SO_RCVTIMEO 和 SO_SNDTIMEO,它只在 Ruby 1.8 上按预期超时。在 1.9 和 2.1 上它没有,只有 IO.select 工作。使用 C,它在 OS X 10.9.4 上按预期工作,但在 Ubuntu 14.04 上却没有 - moret.pro.br/2014/09/03/socket-read-timeout【参考方案2】:

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

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

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

【讨论】:

【参考方案3】:

我认为你基本上不走运。当我使用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 中做类似的事情,所以我不知道我可能忽略的所有陷阱——特别是,我怀疑我在 @987654333 中指定的类型@ 以及我传递缓冲区以读取的方式。

【讨论】:

赞成,感谢您的回答。我希望这不是事实。有许多 Ruby 程序依赖于这种行为,如果是这种情况,它们肯定无法正常工作

以上是关于通过 SO_RCVTIMEO 套接字选项在 Ruby 中设置套接字超时的主要内容,如果未能解决你的问题,请参考以下文章

为啥 SO_RCVTIMEO 从侦听套接字继承到接受的套接字? [关闭]

使用SO_REVTIMEO套接字选项为recvfrom设置超时

如何在 C 中设置 SO_RCVTIMEO 选项仅用于读取而不用于接受

使用socket选项SO_RCVTIMEO和SO_SNDTIMEO设置超时时间

Linux(程序设计):60---定时机制之SO_RCVTIMEOSO_SNDTIMEO选项(附设置connect超时时间案例)

socket_connect没有超时