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

Tyl*_*ock 19 ruby sockets io timeout tcp-ip

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

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

我读过迈克·佩勒姆对这个问题的优秀岗位在这里,并在努力把问题缩小到可运行代码的一个文件中创建一个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
Run Code Online (Sandbox Code Playgroud)

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

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

sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO).inspect
Run Code Online (Sandbox Code Playgroud)

设置此套接字选项实际上适用于任何人吗?

Tyl*_*ock 24

您可以使用Ruby的IO类有效地执行此操作select.

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

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

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

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

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

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

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

[
  [sockets ready for reading],
  [sockets ready for writing],
  [sockets raising errors]
]
Run Code Online (Sandbox Code Playgroud)

但是,nil如果给出了可选的超时值并且在超时秒内没有准备好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
Run Code Online (Sandbox Code Playgroud)

这有不抽丝为每个超时一个新的线程(如超时模块)的利益,并会与许多超时多线程应用程序用Ruby更快.

  • 通过将套接字选项SO_RCVTIMEO和SO_SNDTIMEO与Ruby一起使用,仅在Ruby 1.8上才按预期超时。在1.9和2.1上没有,只有IO.select有效。使用C,它可以在OS X 10.9.4上正常运行,但不能在Ubuntu 14.04上运行-http://moret.pro.br/2014/09/03/socket-read-timeout/ (3认同)

Adi*_*ann 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
Run Code Online (Sandbox Code Playgroud)

strace还显示了什么阻止了该计划.这是我在服务器超时之前在屏幕上看到的行:

[pid  5958] ppoll([{fd=5, events=POLLIN}], 1, NULL, NULL, 8
Run Code Online (Sandbox Code Playgroud)

这意味着该程序阻止此呼叫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
Run Code Online (Sandbox Code Playgroud)

这段代码在这里工作.当然:这是一个丑陋的解决方案,它不适用于许多平台,等等.它可能是一个出路.

还请注意,这是我第一次在Ruby中做类似的事情,所以我不知道我可能忽略的所有陷阱 - 特别是,我怀疑我指定的类型'long read(int, void *, long)'和我的方式传递一个缓冲区来阅读.


Kei*_*ett 6

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

您的操作系统还提供本机套接字超时,可通过SNDTIMEO和RCVTIMEO套接字选项进行设置.但是,从Ruby 1.9开始,这个功能已不再具备功能."

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