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