In Ruby il networking a basso livello è gestito dalla libreria standard socket
che è strutturata nel seguente modo: alla base c'è BasicSocket
classe astratta, sottoclasse di IO
, che contiene alcuni metodi fondamentali, ereditati da tutte le sue sottoclassi, come ad esempio close_read
, close_write
, getpeername
, getsockname
, recv
e send
.
Da BasicSocket discendono direttamente IPSocket
, Socket
e UNIXSocket
. IPSocket
è la classe che implementa le socket che usano il protocollo di trasporto IP e ha due sottoclassi: TCPSocket
e UDPSocket
che rispettivamente trattano connessioni da, e verso, socket TCP e UDP. La classe Socket
fornisce invece direttamente accesso all'implementazione delle socket del sistema operativo. UnixSocket
infine gestisce le comunicazioni IPC utilizzando lo UNIX domain protocol.
Tutte le classi di socket discendono indirettamente dalla classe IO, questo vuol dire che è possibile usare i metodi di IO sui socket così come accade ad esempio per i file.
Invece di dare uno sguardo in dettaglio ai metodi di queste classi vediamo degli utili e semplici esempi che ne illustrano brevemente l'utilizzo. Per ragioni di spazio e opportunità non tratteremo le BSD socket API dandone per scontato l'utilizzo elementare. E per una panoramica delle suddette classi si rimanda alla documentazione ufficiale della libreria standard.
UDP server
Iniziamo con il classico esempio client-server, in questo caso il server invierà al client una frase generata dal programma "fortune", tipo messaggino del dolcetto della fortuna al ristorante cinese. Ecco il server:
require "socket" server = UDPSocket.open server.bind(nil, 12345) loop do data, sender = server.recvfrom(1) chost = sender[3] cport = sender[1] fortunecookie = 'fortune' puts "Request from #{chost}:#{cport}" server.send(fortunecookie, 0, chost, cport) end
Innanzitutto creiamo una nuova istanza della classe UDPSocket
, in questo caso open
è sinonimo di new
e poi con bind
leghiamo la connessione UDP ad un hostname e ad una porta.
Il ciclo principale del server non fa altro che mettersi in attesa di una connessione e lo fa con recvfrom
che prende come argomento il numero di byte da leggere dal socket e restituisce un array contenete i dati ricevuti e le informazioni sul client.
Proprio da queste informazioni ricaviamo l'host e la porta del client che utilizziamo con send per rispondere al client; send prende come argomenti i dati da inviare, alcune opzioni, l'hostname e la porta del destinatario. Questo è invece il client:
require "socket" client = UDPSocket.open client.connect('localhost', 12345) client.send("", 0) while client.gets puts $_ end
In questo caso dopo aver creato un oggetto di tipo UDPSocket
creiamo una connessione verso localhost sulla porta 12345
e inviamo una richiesta con send utilizzando la versione a due parametri e passando una stringa vuota dato che il nostro server non si cura di questo parametro. Infine ci mettiamo in ascolto della risposta del server che stampiamo a video. Anche in questo caso avremmo potuto passare a send anche l'hostname e la porta e quindi avremmo dovuto sostituire le righe
client.connect('localhost', 12345) client.send("", 0)
con la riga
client.send("", 0, 'localhost', 12345)
Ecco ora una breve sessione delle nostre mini-applicazioni appena scritte:
$ ruby UDPserver.rb Request from 127.0.0.1:34317
e allo stesso momento il client otterrà una risposta:
$ ruby UDPclient.rb I still maintain the point that designing a monolithic kernel in 1991 is a fundamental error. Be thankful you are not my student. You would not get a high grade for such a design :-) (Andrew Tanenbaum to Linus Torvalds)
TCP server
Analogamente un server e un client TCP vanno scritti nel seguente modo:
require "socket" server = TCPServer.open('localhost', 12345) while session = server.accept fortunecookie = `fortune -e linuxcookiè session.puts fortunecookie session.close end
In questo caso abbiamo utilizzato la sottoclasse TCPServer
di TCPSocket
, ne abbiamo creato un istanza con open passando la porta e l'hostname come argomenti. Dopodiché, nel ciclo principale, con accept
ci siamo messi in attesa di una connessione identificata da session che è di tipo TCPSocket
. Infine mandiamo la frase generata da fortune e chiudiamo la connessione.
Il client conterrà del codice di questo tipo:
require "socket" client = TCPSocket.open('localhost', 12345) while client.gets puts $_ end client.close
Il client è molto semplice, non facciamo altro che aprire una connessione TCP verso localhost sulla porta 12345
e poi stampiamo la risposta inviata dal server. Anche in questo caso una breve sessione delle nostre applicazioni:
$ ruby TCPserver.rb $ ruby TCPclient.rb Dijkstra probably hates me (Linus Torvalds, in kernel/sched.c)
Le differenze principali tra le due coppie client-server sono dovute essenzialmente alla diversa natura dei protocolli TCP e UDP. Lascio come utile esercizio al lettore l'implementazione di un server TCP con la gestione delle richieste attraverso i thread.