Установите время ожидания сокета в Ruby с помощью опции сокета SO_RCVTIMEO - PullRequest
18 голосов
/ 24 марта 2012

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

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

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

I 'мы пытались сделать то же самое с TCPSocket (который подключается автоматически) и видели подобный код в redis и других проектах.

Кроме того, я могу убедиться, что опция установленапозвонив по номеру getsockopt следующим образом:

sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO).inspect

Работает ли настройка этого сокета на самом деле у кого-нибудь?

Ответы [ 3 ]

24 голосов
/ 24 августа 2012

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

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

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

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

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

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

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

Таким образом, возвращаемое значение 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.

6 голосов
/ 11 октября 2013

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

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

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

6 голосов
/ 24 марта 2012

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

...