2012-03-24 15 views
17

Sto provando a rendere il timeout dei socket in Ruby tramite l'opzione socket SO_RCVTIMEO, tuttavia sembra che non abbia alcun effetto su alcun sistema operativo * nix recente.Imposta il timeout del socket in Ruby tramite SO_RCVTIMEO opzione socket

L'utilizzo del modulo Timeout di Ruby non è un'opzione in quanto richiede spawn e unisce i thread per ciascun timeout che può diventare costoso. Nelle applicazioni che richiedono timeout di socket bassi e che hanno un numero elevato di thread, uccide essenzialmente le prestazioni. Questo è stato notato in molti posti tra cui Stack Overflow.

Ho letto l'eccellente post di Mike Perham sull'oggetto here e nel tentativo di ridurre il problema a un file di codice eseguibile creato un semplice esempio di un server TCP che riceverà una richiesta, attendi la quantità di tempo inviata nella richiesta e quindi chiudere la connessione.

Il client crea un socket, imposta il timeout di ricezione su 1 secondo e quindi si collega al server. Il client dice al server di chiudere la sessione dopo 5 secondi, quindi attende i dati.

Il cliente deve timeout dopo un secondo, ma invece chiude con successo la connessione dopo 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 

Ho provato a fare la stessa cosa con un TCPSocket così (che si connette automaticamente) e hanno visto codice simile in redis e altri progetti.

Inoltre, posso verificare che l'opzione è stata impostata chiamando getsockopt come questo:

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

non impostare questa opzione presa in realtà il lavoro per chiunque?

+0

Questo tipo di questione è stata postato prima, e sembra che la risposta migliore sia stata usare la libreria 'timeout' di Ruby attorno alla chiamata' recv'. – Linuxios

+0

No, usare la libreria di timeout genera i thread in modo che ogni timeout richieda la creazione e la distruzione di un thread che può diventare molto costoso. aggiornerò la mia risposta per riflettere sul fatto che non è efficiente –

risposta

22

È possibile eseguire questa operazione in modo efficiente utilizzando select dalla classe IO di Ruby.

IO::select accetta 4 parametri. I primi tre sono array di socket da monitorare e l'ultimo è un timeout (specificato in secondi).

Il modo in cui select funziona è che rende gli elenchi di oggetti IO pronti per una determinata operazione bloccando finché almeno uno di essi è pronto per essere letto, scritto o che desidera generare un errore.

I primi tre argomenti corrispondono quindi ai diversi tipi di stati da monitorare.

  • pronto per la lettura
  • pronto per la scrittura
  • Ha attesa eccezione

Il quarto è il timeout che si desidera impostare (se presente). Stiamo per trarre vantaggio da questo parametro.

Seleziona restituisce un array che contiene matrici di oggetti IO (prese in questo caso) che sono considerate pronte dal sistema operativo per la particolare azione monitorata.

Quindi il valore di ritorno di selezionare sarà simile a questa:

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

Tuttavia, selezionare i rendimenti nil se il valore di timeout facoltativo è dato e nessun oggetto IO è pronto in pochi secondi di timeout.

Pertanto, se si vuole fare performante IO timeout in Ruby ed evitare di dover utilizzare il modulo Timeout, è possibile effettuare le seguenti operazioni:

Costruiamo un esempio dove ci aspetta timeout secondi per una lettura su socket:

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

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

Questo ha il vantaggio di non gira un nuovo thread per ogni timeout (come nel modulo timeout) e renderà applicazioni multi-thread con molto timeout molto veloci in rubino.

+0

Si supponga che le letture debbano essere readpartial o read_nonblock – nhed

+0

So che questo è un thread vecchio a questo punto, ma come potrebbe funzionare con timeout che sembrano accadere su un SSL .connect()? – Justin

+0

Inviami un'e-mail e sarei felice di aiutarti. –

6

Penso che tu sia praticamente sfortunato. Quando eseguo il tuo esempio con strace (solo utilizzando un server esterno per mantenere l'uscita pulita), è facile verificare che setsockopt è infatti sempre chiamato:

$ 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 mostra anche ciò che sta bloccando il programma. Questa è la linea che vedo sullo schermo prima dei tempi di server fuori:

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

Ciò significa che il programma sta bloccando in questa chiamata a ppoll, non su una chiamata a recvfrom. La pagina man in cui sono elencate le opzioni di socket (socket(7)) stabilisce che:

Timeout non hanno effetto per prescelto (2), poll (2), epoll_wait (2), ecc

Così il timeout viene impostato ma non ha alcun effetto. Spero di sbagliarmi qui, ma sembra che non ci sia modo di cambiare questo comportamento in Ruby. Ho dato una rapida occhiata all'implementazione e non ho trovato una via d'uscita ovvia. Di nuovo, spero di sbagliarmi - questo sembra essere qualcosa di fondamentale, come mai non è lì?

Una soluzione (molto brutta) consiste nell'utilizzare dl per chiamare direttamente read o recvfrom. Queste chiamate sono influenzate dal timeout impostato.Ad esempio:

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 

Questo codice funziona qui. Certo: è una brutta soluzione, che non funziona su molte piattaforme, ecc. Potrebbe essere una via d'uscita però.

Si noti inoltre che questa è la prima volta che faccio qualcosa di simile in Ruby, quindi non sono a conoscenza di tutte le insidie ​​che potrei trascurare - in particolare, sono sospetto dei tipi che ho specificato in 'long read(int, void *, long)' e del modo in cui sto passando un buffer da leggere.

+0

In aumento, grazie per aver risposto. Spero che non sia la verità. Ci sono molti programmi Ruby che dipendono da questo comportamento che non deve funzionare correttamente se questo è il caso –

6

Sulla base della mia prova, e l'eccellente ebook di Jesse Storimer su "Lavorare con Socket TCP" (in Ruby), le opzioni di socket timeout non funzionano in Ruby 1.9 (e, presumo 2.0 e 2.1). Jesse dice:

Il sistema operativo offre anche i timeout di socket nativi che possono essere impostati tramite il SNDTIMEO e opzioni socket RCVTIMEO. Ma, a partire da Ruby 1.9, questa funzione non è più funzionale ".

Wow. Penso che la morale della storia è quello di dimenticarsi di queste opzioni e utilizzare IO.select o una libreria NIO di Tony Arcieri.