Установите время ожидания сокета в Ruby с помощью опции сокета SO_RCVTIMEO

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

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

Я прочитал замечательный пост Майка Перхама по этому вопросу здесь и, пытаясь свести проблему к одному файлу с исполняемым кодом, создал простой пример 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

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

3 ответа

Решение

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

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

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

Таким образом, первые три аргумента соответствуют различным типам состояний, которые необходимо отслеживать.

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

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

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

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

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

Однако выберите возврат nil если задано необязательное значение тайм-аута, и ни один объект ввода-вывода не готов в течение секунд ожидания.

Таким образом, если вы хотите выполнить тайм-ауты ввода-вывода в 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 будут намного быстрее.

Основываясь на моем тестировании и превосходной книге Джесси Стоимера "Работа с TCP-сокетами" (в Ruby), опции сокетов тайм-аута не работают в Ruby 1.9 (и, я полагаю, 2.0 и 2.1). Джесси говорит:

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

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

Я думаю, что вам в основном не повезло. Когда я запускаю твой пример с 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 звонить 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)' и, кстати, я передаю буфер для чтения.

Другие вопросы по тегам