2012-03-24 20 views
17

Estoy intentando hacer que el socket expire en Ruby a través de la opción de socket SO_RCVTIMEO; sin embargo, parece que no tiene ningún efecto en ningún sistema operativo * nix reciente.Establecer el tiempo de espera del socket en Ruby mediante la opción de socket SO_RCVTIMEO

El uso del módulo de tiempo de espera de Ruby no es una opción, ya que requiere engendrar y unir hilos por cada tiempo de espera que puede llegar a ser costoso. En aplicaciones que requieren tiempos de espera de socket bajos y que tienen un alto número de subprocesos, esencialmente se mata el rendimiento. Esto se ha observado en muchos lugares, incluido Stack Overflow.

He leído excelente publicación de Mike Perham sobre el tema here y en un esfuerzo por reducir el problema a un archivo de código ejecutable creado un ejemplo simple de un servidor TCP que recibirá una solicitud, espere el tiempo enviado en la solicitud y luego cierre la conexión.

El cliente crea un socket, establece el tiempo de espera de recepción de 1 segundo y luego se conecta al servidor. El cliente le dice al servidor que cierre la sesión después de 5 segundos y luego espera datos.

El cliente debe tiempo de espera después de un segundo, pero en su lugar se cierra con éxito la conexión después de 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 

He intentado hacer lo mismo con un TCPSocket así (que se conecta de forma automática) y han visto un código similar en redis y otros proyectos.

Además, puedo comprobar que la opción se ha establecido llamando getsockopt así:

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

lo hace configurar esta opción toma realmente funciona para cualquier persona?

+0

Este tipo de pregunta ha sido publicado antes, y parece que la mejor respuesta fue utilizar la biblioteca 'timeout' de Ruby alrededor de la llamada' recv' – Linuxios

+0

No, el uso de la biblioteca de tiempo de espera genera hilos de modo que cada tiempo de espera requiere crear y destruir un hilo que puede ser muy costoso. actualizaré mi respuesta para reflejar que no es eficiente. –

Respuesta

22

Puede hacer esto eficientemente usando select de la clase IO de Ruby.

IO::select toma 4 parámetros. Los primeros tres son arreglos de sockets para monitorear y el último es un tiempo de espera (especificado en segundos).

La selección de la forma funciona es que hace que las listas de objetos IO estén listas para una operación dada mediante el bloqueo hasta que al menos una de ellas esté lista para ser leída, escrita o desee generar un error.

Por lo tanto, los primeros tres argumentos corresponden a los diferentes tipos de estados a monitorear.

  • listo para la lectura
  • listo para escribir
  • tiene pendiente excepción

La cuarta es el tiempo de espera que desea establecer (si lo hay). Vamos a aprovechar este parámetro.

Seleccionar devuelve una matriz que contiene matrices de objetos IO (sockets en este caso) que el sistema operativo considera listas para la acción particular que se supervisa.

lo tanto, el valor de retorno de select se verá así:

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

Sin embargo, select regrese nil si se le da el valor de tiempo de espera opcional y no hay ningún objeto IO está listo en cuestión de segundos de tiempo de espera.

Por lo tanto, si usted quiere hacer performant IO tiempos de espera en Ruby y evitar tener que utilizar el módulo de tiempo de espera, puede hacer lo siguiente:

Construyamos un ejemplo donde esperamos timeout segundos para una lectura en socket:

ready = IO.select([socket], nil, nil, timeout) 

if ready 
    # do the read 
else 
    # raise something that indicates a timeout 
end 

Esto tiene la ventaja de no girar hasta un nuevo hilo para cada tiempo de espera (como en el módulo de tiempo de espera) y hará que las aplicaciones de subprocesos múltiples con muchos tiempos de espera mucho más rápidas en Ruby.

+0

Supongamos que las lecturas deben ser readpartial o read_nonblock – nhed

+0

Sé que este es un hilo antiguo en este momento, pero ¿cómo podría funcionar esto con los tiempos de espera que parecen ocurrir en un SSL?()? – Justin

+0

Envíeme un correo electrónico y con gusto lo ayudaré. –

6

Creo que básicamente no tienes suerte. Cuando corro con tu ejemplo strace (sólo utiliza un servidor externo para mantener la salida limpia), es fácil comprobar que efectivamente se setsockopt recibiendo llamadas:

$ 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 también muestra lo que está bloqueando el programa. Esta es la línea que veo en la pantalla antes de los tiempos de servidor de salida:

[pid 5958] ppoll([{fd=5, events=POLLIN}], 1, NULL, NULL, 8 

Eso significa que el programa está bloqueando en esta llamada a ppoll, no en una llamada a recvfrom. La página del manual que enumera las opciones de socket (socket(7)) establece que:

tiempos de espera no tienen ningún efecto de selección (2), poll (2), epoll_wait (2), etc.

lo tanto, el tiempo de espera se está configurando pero no tiene efecto. Espero estar equivocado aquí, pero parece que no hay forma de cambiar este comportamiento en Ruby. Eché un vistazo rápido a la implementación y no encontré una salida obvia. Nuevamente, espero estar equivocado, esto parece ser algo básico, ¿cómo es que no está allí?

Una solución (muy fea) es usar dl para llamar directamente al read o recvfrom. Esas llamadas se ven afectadas por el tiempo de espera que establece.Por ejemplo:

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 

Este código funciona aquí. Por supuesto: es una solución fea, que no funcionará en muchas plataformas, etc. Sin embargo, puede ser una salida.

También tenga en cuenta que esta es la primera vez que hago algo similar en Ruby, así que no estoy al tanto de todas las trampas que puedo pasar por alto, en particular, sospecho de los tipos que especifiqué en 'long read(int, void *, long)' y de la forma en que estoy pasando un buffer para leer.

+0

Upvoted, gracias por responder. Espero que no sea cierto. Hay muchos programas de Ruby que dependen de esta b comportamiento que no debe funcionar correctamente si este es el caso –

6

Basado en mis pruebas, y excelente libro electrónico de Jesse Storimer en "Trabajar con sockets TCP" (en Ruby), las opciones de los conectores de tiempo de espera no funcionan en Ruby 1.9 (y, supongo 2.0 y 2.1). Jesse dice:

su sistema operativo también ofrece tiempos de espera de socket nativas que se pueden establecer a través de la SNDTIMEO y opciones de conector RCVTIMEO. Pero, a partir de Ruby 1.9, esta función ya no es funcional."

Wow. Creo que la moraleja de la historia es olvidarse de estas opciones y utilizar IO.select o biblioteca NIO de Tony Arcieri.

Cuestiones relacionadas