Establezca el tiempo de espera del socket en Ruby a través de la opción SO_RCVTIMEO socket
sockets io (3)
Estoy tratando de hacer que el tiempo de espera de los sockets en Ruby a través de la opción de socket SO_RCVTIMEO parezca que no tiene ningún efecto en ningún sistema operativo * nix reciente.
El uso del módulo de tiempo de espera de Ruby no es una opción, ya que requiere generar y unir hilos para cada tiempo de espera, lo que puede resultar costoso. En aplicaciones que requieren tiempos de espera de socket bajos y que tienen una gran cantidad de subprocesos, esencialmente mata el rendimiento. Esto se ha observado en muchos lugares, incluido el desbordamiento de pila .
He leído la 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, se creó un ejemplo simple de un servidor TCP que recibirá una solicitud, espere la cantidad de tiempo enviada en la solicitud y a continuación, cierre la conexión.
El cliente crea un socket, establece el tiempo de espera de recepción en 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 los datos.
El cliente debe agotar el tiempo de espera después de un segundo, pero en cambio, cierra la conexión con éxito 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
También he intentado hacer lo mismo con un TCPSocket (que se conecta automáticamente) y he visto un código similar en redis y otros proyectos.
Además, puedo verificar que la opción se haya establecido llamando a getsockopt
esta forma:
sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO).inspect
¿Configurar esta opción de socket realmente funciona para alguien?
Creo que básicamente estás fuera de suerte. Cuando ejecuto su ejemplo con strace
(solo uso un servidor externo para mantener la salida limpia), es fácil verificar que setsockopt
sea llamado:
$ 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 que se agote el tiempo del servidor:
[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 de manual que enumera las opciones de socket(7) ) indica que:
Los tiempos de espera no tienen efecto para seleccionar (2), sondear (2), epoll_wait (2), etc.
Así que el tiempo de espera se está estableciendo, pero no tiene ningún 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. Una vez más, espero que me equivoque, esto parece ser algo básico, ¿por qué no está ahí?
Una solución muy (muy fea) es usar dl
para llamar a read
o recvfrom
directamente. Esas llamadas se ven afectadas por el tiempo de espera establecido. 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. Aunque puede ser una salida.
También tenga en cuenta que esta es la primera vez que hago algo similar en Ruby, por lo que no estoy al tanto de todas las dificultades que puedo pasar por alto, en particular, soy sospechoso de los tipos que especifiqué en ''long read(int, void *, long)''
y de la forma en que estoy pasando un buffer para leer.
Puede hacerlo de manera eficiente utilizando la select
de la clase IO de Ruby.
IO::select
toma 4 parámetros. Los tres primeros son matrices de sockets para monitorear y el último es un tiempo de espera (especificado en segundos).
La forma en que funciona la selección es que hace que las listas de objetos de E / S estén listas para una operación determinada bloqueando hasta que al menos uno de ellos esté listo para ser leído, escrito o quiera generar un error.
Los tres primeros argumentos, por lo tanto, corresponden a los diferentes tipos de estados a monitorear.
- Listo para leer
- Listo para escribir
- Tiene la excepción pendiente
El cuarto es el tiempo de espera que desea establecer (si corresponde). Vamos a aprovechar este parámetro.
Select devuelve una matriz que contiene matrices de objetos IO (sockets en este caso) que el sistema operativo considera que están listos para la acción en particular que se está monitoreando.
Así que el valor de retorno de select se verá así:
[
[sockets ready for reading],
[sockets ready for writing],
[sockets raising errors]
]
Sin embargo, select devuelve nil
si se da el valor de tiempo de espera opcional y ningún objeto IO está listo en segundos de tiempo de espera.
Por lo tanto, si desea realizar tiempos de espera de IO en Ruby y evitar tener que usar el módulo de tiempo de espera, puede hacer lo siguiente:
Construyamos un ejemplo donde esperemos segundos de timeout
para una lectura en el 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 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 sean mucho más rápidas en Ruby.
Según mis pruebas y el excelente libro electrónico de Jesse Storimer sobre "Trabajar con sockets TCP" (en Ruby), las opciones de socket de tiempo de espera no funcionan en Ruby 1.9 (y, supongo que 2.0 y 2.1). Jesse dice:
Su sistema operativo también ofrece tiempos de espera de socket nativos que se pueden configurar a través de las opciones de socket SNDTIMEO y RCVTIMEO. Pero, a partir de Ruby 1.9, esta característica ya no es funcional ".
Guau. Creo que la moraleja de la historia es olvidarse de estas opciones y usar la biblioteca NIO de IO.select
o Tony Arcieri.