Подтвердить что ты не робот

Установите тайм-аут сокета в Ruby через опцию SO_RCVTIMEO

Я пытаюсь сделать тайм-аут сокетов в Ruby через опцию сокета SO_RCVTIMEO, но, похоже, он не влияет на какую-либо новую операционную систему * nix.

Использование модуля тайм-аута Ruby не является вариантом, так как требует нереста и объединения потоков для каждого таймаута, который может стать дорогостоящим. В приложениях, требующих минимальных тайм-аутов сокетов и имеющих большое количество потоков, он по существу убивает производительность. Это было отмечено во многих местах, включая Переполнение стека.

Я прочитал 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

Действительно ли настройка этого параметра сокета работает для всех?

4b9b3361

Ответ 1

Вы можете сделать это эффективно, используя select из класса Ruby IO.

IO::select принимает 4 параметра. Первые три представляют собой массивы сокетов для мониторинга, а последний - это тайм-аут (указанный в секундах).

Способ выбора - это то, что он делает списки объектов IO готовыми к заданной операции, блокируя, пока, по крайней мере, один из них не будет готов к чтению, написанию или попытке поднять ошибку.

Поэтому первые три аргумента соответствуют различным типам состояний для мониторинга.

  • Готов к чтению
  • Готов к записи
  • Имеет ожидающее исключение

Четвертый - это таймаут, который вы хотите установить (если есть). Мы будем использовать этот параметр.

Select возвращает массив, содержащий массивы объектов IO (в этом случае сокеты), которые операционная система считается готовой для конкретного контролируемого действия.

Таким образом, возвращаемое значение select будет выглядеть следующим образом:

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

Однако, select возвращает nil, если задано дополнительное значение тайм-аута, и IO-объект не готов в течение таймаута секунд.

Поэтому, если вы хотите выполнить синтаксические тайм-ауты ввода-вывода в Ruby и не использовать модуль 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.

Ответ 2

Думаю, тебе в основном не повезло. Когда я запускаю ваш пример с помощью 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)), указывает, что:

Таймауты не влияют на выбор (2), опрос (2), epoll_wait (2) и т.д.

Таким образом, тайм-аут устанавливается, но не влияет. Надеюсь, я здесь не прав, но, похоже, нет никакого способа изменить это поведение в Ruby. Я быстро взглянул на реализацию и не нашел очевидного выхода. Опять же, я надеюсь, что я ошибаюсь - это, кажется, что-то основное, как это не так?

Один (очень уродливый) обходной путь заключается в использовании dl для прямого вызова read или recvfrom. На эти вызовы влияет установленный вами тайм-аут. Например:

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)' и способа передачи буфера для чтения.

Ответ 3

На основе моего тестирования и Jesse Storimer отличная книга "Работа с TCP-сокетами" (в Ruby), параметры тайм-аута не работают в Ruby 1.9 (и, я полагаю, 2.0 и 2.1). Джесси говорит:

В вашей операционной системе также есть встроенные тайм-ауты, которые можно установить через SNDTIMEO и RCVTIMEO. Но с Ruby 1.9 эта функция больше не работает функциональный ".

Ого. Я думаю, что мораль этой истории заключается в том, чтобы забыть об этих вариантах и ​​использовать библиотеку IO.select или Tony Arcieri NIO.