Programmation des sockets en Ruby - Éric Jacoboni Université du Mirail, IUP NTIE, Toulouse
←
→
Transcription du contenu de la page
Si votre navigateur ne rend pas la page correctement, lisez s'il vous plaît le contenu de la page ci-dessous
Programmation des sockets en Ruby Éric Jacoboni Université du Mirail, IUP NTIE, Toulouse Rappels Avec TCP et UDP, une connexion est entièrement définie par 5 paramètres : Le type du protocole utilisé (TCP ou UDP). L'adresse IP de la machine source. Le numéro de port associé au processus sur la machine source. L'adresse IP de la machine destinataire. Le numéro de port associé au processus sur la machine destinataire. Clients et serveurs Les communications Internet impliquent souvent un client et un serveur. La communication a toujours lieu a l'initiative du client. Avec TCP, le client et le serveur communiquent après avoir établi une connexion :chacun d'eux connaît donc l'adresse et le port de l'autre. Envoyer/recevoir des données revient donc à écrire/lire dans un « canal » précédemment établi. Avec UDP, ils fonctionnent sans connexion. C'est au moment de la réception que le serveur peut identifier l'adresse et le port de son client et qu'il peut donc utiliser ces informations pour lui répondre. Communication côté client Le protocole étant commun aux deux applications, une communication se traduit donc, localement, pour un client, par 3 valeurs : Le type du protocole utilisé (TCP ou UDP). L'adresse IP de la machine serveur. Le numéro de port associé au processus serveur sur la machine distante. Les deux autres informations sont fournies par le système : Le client connaît déjà son adresse IP. Le port du client est alloué dynamiquement par le SE. Communication côté serveur Pour un serveur, localement, 2 valeurs suffisent : Le type du protocole utilisé (TCP ou UDP). Le numéro de port sur lequel on attend les connexions. Les trois autres informations sont fournies par le système : Le serveur connait déjà son adresse IP. Il connaît l'adresse et le port du client (soit par la connexion TCP, soit par la requête UDP).
Présentation des sockets (1/2) Les sockets (prises bidirectionnelles) permettent à des applications de communiquer entre elles. Elles ont été introduites par Unix BSD. Les applications désirant communiquer créent chacune une socket, qui n'est rien d'autre qu'un descripteur de fichier. Elles envoient et reçoivent les données par leur socket en utilisant des primitives classiques read()/write() ou par des opérations spécifiques. Les sockets sont des mécanismes de communication permettant à des programmes d'échanger des données. Elles sont des points d'accès à la couche transport (userland) ou à la couche réseau (noyau). Présentation des sockets (2/2) Deux types de sockets aux fonctionnements très semblables : Les sockets de domaine Unix (AF_UNIX), permettent les échanges entre deux programmes sur la même machine en faisant référence à un nom de fichier. les sockets de domaine Internet (AF_INET) permettent les échanges via le réseau. Ce sont celles qui sont présentées ici. Les sockets Internet sont essentiellement de trois types : SOCK_STREAM (TCP), SOCK_DGRAM (UDP), SOCK_RAW (couche réseau). Dans ce cours, nous ne présenterons que les SOCK_STREAM et les SOCK_DGRAM. Adresse des sockets Pour communiquer, il faut créer une socket locale et la brancher sur une socket distante. Chaque socket est décrite par une adresse de socket dont la représentation varie en fonction du niveau d'abstraction : Elle est représentée par un struct sockaddr_in en C. Dans des langages de plus haut niveau (Perl, Python, Ruby), cette adresse est représentée directement comme un tableau. En Java, cette adresse est décrite par la classe InetSocketAddress qui se construit à partir de deux informations, l'adresse et le port. En C#, cette adresse est décrite par la classe IPEndPoint construite à partir d'une IPAddress et d'un numéro de port. L'API de bas niveau, en C (1/4) Une socket est décrite par une struct sockaddr_in, dont l'initialisation est la partie la plus pénible de la manipulation des sockets avec l'API C. On verra plus loin des exemples de cette structure. Le reste est assez simple : on fait les appels dans le bon ordre et on n'oublie pas de fermer la socket. Le plus difficile étant de manipuler la structure struct sockaddr_in, nous avons préféré utiliser un langage évolué, Ruby, afin de nous concentrer sur les concepts plutôt que sur la gestion des pointeurs... L'API de bas niveau, en C (2/4) socket(2) : création d'une socket locale, qui sera décrite par un descripteur de fichier, comme open(2) et pipe(2).
connect(2) : avec TCP, connecte la socket locale à une socket distante (qui doit être en écoute) ; avec UDP, sert uniquement à créer une association avec une socket distante (la socket locale ne pourra plus lire et écrire que sur cette socket distante). read(2), recv(2) et recvfrom(2) : lecture sur la socket locale. Avec une socket UDP seule recvfrom(2) est possible (elle permet de récupérer les informations sur l'expéditeur). Ces appels sont bloquants (comme avec les tubes) et sont tamponnés (ne pas oublier d'envoyer des \r\n en fin d'émission). L'API de bas niveau, en C (3/4) write(2), send(2), sendto(2) : écriture sur la socket. Avec une socket UDP, seule sendto(2) est possible (elle permet de d'indiquer la socket distante). close(2) et shutdown(2) : fermeture de la socket. shutdown(2) permet de ne fermer qu'une ou l'autre des extrémités, ou les deux. L'oubli de fermeture d'une socket peut être clause de blocage ! (comme pour les tubes). bind(2) : attachement d'une socket locale à un port local. Généralement, uniquement pour les applications serveurs (les clients utilisent le plus souvent des ports alloués dynamiquement par le système). L'API de bas niveau, en C (4/4) listen(2) : fixe la taille de la file d'attente des connexions pour une application serveur (uniquement pour TCP). accept(2) : l'application serveur TCP se bloque tant qu'il n'y a pas de connexion dans la file d'attente. Sinon, cet appel extrait la première demande de la file, crée une nouvelle socket et renvoie son descripteur. Celui-ci permet alors de communiquer avec celui qui a demandé cette connexion. Schéma de communication UDP
Schéma de communication TCP Remarques sur le schéma Ce schéma part du principe que c'est le client qui met fin à la communication (cas du protocole TELNET, par exemple). Avec certains protocoles (HTTP, ECHO, etc.), c'est le serveur qui met fin à la connexion après avoir envoyé sa réponse. Dans ce dernier cas, si le client tente de continuer à lire sans tester la fin de fichier (read renvoie 0), il est bloqué. Création d'une socket en Ruby En Ruby, bien qu'il soit possible de mimer tous les appels systèmes des sockets C (via la classe socket), on crée généralement une instance de la classe TCPSocket, TCPServer ou UDPSocket. On peut utiliser la méthode new ou open. On a deux façons de procéder : ouverture avec new, communication, fermeture avec close ou shutdown. ouverture avec open en lui associant un bloc. En ce cas, la socket sera automatiquement fermée à la fin du bloc. Exemple Utilisation « classique » : >> require 'socket' >> conn = TCPSocket.new("www.free.fr", 80) >> ... on cause en HTTP au serveur en passant par conn >> conn.shutdown Utilisation des blocs Ruby : >> TCPSocket.open("www.free.fr", 80) do |conn|
>> ... on cause en HTTP au serveur en passant par conn >> end # Fin du bloc => fermeture de conn Évidemment, dans ces deux cas, il faudrait gérer les exceptions (serveur injoignable, par exemple). Ruby : envoi des données Avec Ruby, on écrit dans une socket TCP comme dans un fichier, avec les méthodes print, puts ou printf, par exemple. De même, on écrit dans une socket UDP en utilisant la méthode send à qui l'on passe la chaîne, l'hôte destinataire, le port destinataire et des options (0 si aucune). On peut également utiliser la méthode connect sur une socket UDP afin de préciser l'hôte destinataire et son port, auquel cas on n'aura plus besoin de fournir ces informations ensuite à la méthode send. Ne pas oublier de faire : require 'socket' Ruby : réception des données Avec Ruby, on lit dans une socket TCP comme dans tout instance de la classe IO, le plus souvent avec la méthode gets, mais également avec read, readline ou readlines. On lit dans une socket UDP en utilisant la méthode recvfrom à qui l'on passe une longueur et des options (0 si aucune). La longueur est le nombre d'octets max que l'on s'attend à recevoir. Cet appel renvoie un tableau de la forme [données, expéditeur] où expéditeur est un tableau décrivant la socket émettrice. Ruby : fermeture d'une socket Les différentes classes de sockets disposent aussi des méthodes close et shutdown. shutdown ferme les deux extrémités par défaut (mais on peut utiliser 0 ou 1 pour n'en fermer qu'une). Comme on l'a vu, si une socket est ouverte avec open et un bloc, elle est automatiquement fermée à ses deux extrémités à la fin du bloc. Communication UDP en Ruby
Algorithme d'un serveur UDP Création de la socket. Liaison de la socket à un port d'écoute avec bind. Boucle de réception/traitement/réponse. Fermeture de la socket à l'arrêt du serveur. Exemple : un serveur d'écho UDP (1/2) #! /usr/local/bin/ruby -w require 'socket' TAILLE_TAMPON = 256 # Vérification de la ligne de commande de l'appel raise "Usage: #{$0} \n" if ARGV.length != 1 # Création du socket de réception sock = UDPSocket.open # Mise en place d'un gestionnaire pour capturer SIGINT trap("SIGINT") { sock.close puts "Arrêt du serveur" exit 0 } # Liaison de la socket au port (sur toutes les adresses) sock.bind("", ARGV[0]); puts "Serveur en attente sur le port #{ARGV[0]}" Exemple : un serveur d'écho UDP (2/2) # Boucle d'attente d'un datagramme loop do
# Attente d'un requête requete = sock.recvfrom(TAILLE_TAMPON) raise "Erreur recvfrom()...\n" if requete.length < 0 message, sockClient = requete ipClient = sockClient[3] portClient = sockClient[1] # Affichage d'une info sur le serveur (debug ou log) printf "Requête provenant de %s, longueur = %d\n", ipClient, message.length # Création de la réponse reponse = message.upcase # Envoi de la réponse sock.send(reponse, 0, ipClient, portClient) end La même chose en C... (1/5) # ... les include ont étés omis... #define TAILLE_TAMPON 1000 static int sock; void ErreurFatale(char message[]) { fprintf(stderr, "SERVEUR > Erreur fatale\n"); fprintf(stderr, "%s : %s\n", strerror(errno), message); exit(EXIT_FAILURE); } void ArretServeur(int signal) { close(sock); printf("\nSERVEUR > Arrêt du serveur (signal %d)\n",signal); exit(EXIT_SUCCESS); } La même chose en C... (2/5) int main(int argc, char *argv[]) { struct sockaddr_in adresse_socket_serveur; size_t taille_adresse_socket_serveur; int numero_port_serveur; char *src, *dst; struct sigaction a; /* 1. Réception des paramètres de la ligne de commande */ if (argc != 2) { fprintf(stderr, "Usage: %s \n", basename(argv[0])); exit(EXIT_FAILURE); } numero_port_serveur = atoi(argv[1]); /* 2. Si une interruption se produit, arrêt du serveur */ a.sa_handler = ArretServeur;
sigemptyset(&a.sa_mask); a.sa_flags = 0; sigaction(SIGINT, &a, NULL); La même chose en C... (3/5) /* 3. Initialisation de la socket de réception */ /* 3.1 Création du socket en mode non-connecté (datagrammes) */ if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) ErreurFatale("socket()"); /* 3.2 Remplissage de l'adresse de réception (protocole Internet TCP-IP, réception sur toutes les adresses IP du serveur, numéro de port indiqué) */ adresse_socket_serveur.sin_family = AF_INET; adresse_socket_serveur.sin_addr.s_addr = INADDR_ANY; adresse_socket_serveur.sin_port = htons(numero_port_serveur); /* 3.3 Association du socket au port de réception */ taille_adresse_socket_serveur = sizeof(adresse_socket_serveur); if ( bind(sock, (struct sockaddr *)&adresse_socket_serveur, taille_adresse_socket_serveur) < 0) ErreurFatale("bind()"); La même chose en C... (4/5) printf("SERVEUR > Le serveur écoute le port %d\n", numero_port_serveur); for(;;) { struct sockaddr_in adresse_socket_client; int taille_adresse_socket_client; char tampon_requete[TAILLE_TAMPON], tampon_reponse[TAILLE_TAMPON]; int longueur_requete,longueur_reponse; /* 4. Attente d'un datagramme (requête) */ taille_adresse_socket_client = sizeof(adresse_socket_client); bzero(tampon_requete, TAILLE_TAMPON); longueur_requete = recvfrom(sock, tampon_requete, TAILLE_TAMPON, 0, /* pas de flags */ (struct sockaddr *)&adresse_socket_client, (socklen_t *)&taille_adresse_socket_client); if (longueur_requete < 0) ErreurFatale("recvfrom()"); La même chose en C... (5/5) /* 5. Affichage du message avec sa provenance et sa longueur */ printf("%s:%d [%d]\t: %s\n", inet_ntoa(adresse_socket_client.sin_addr), ntohs(adresse_socket_client.sin_port),
longueur_requete, tampon_requete); /* 6. Fabrication d'une réponse */ src = tampon_requete; dst = tampon_reponse; while ((*dst++ = toupper(*src++)) != '\0'); longueur_reponse = strlen(tampon_reponse) + 1; /* 7. Envoi de la réponse */ if (sendto(sock, tampon_reponse, longueur_reponse, 0, (struct sockaddr *)&adresse_socket_client, taille_adresse_socket_client) < 0) ErreurFatale("Envoi de la réponse"); } /* for(;;) */ } /* main */ Exemple : un client écho UDP require 'socket' TAILLE_TAMPON = 1000 begin raise "Usage : #{$0} " if ARGV.length != 1 sock = UDPSocket.open # Le client n'a pas besoin de lier la socket à un port avec bind # car le port local est alloué dynamiquement par le système. print "Entrez votre message :" message = STDIN.gets.chomp sock.connect("localhost",ARGV[0].to_i) sock.send(message, 0) reponse, sockServeur = sock.recvfrom(TAILLE_TAMPON) puts "Réponse : #{reponse}" rescue Exception e => STDERR.puts e ensure sock.close if sock # Fermeture extrémité locale end La même chose en Java... (1/3) import java.io.*; import java.net.*; public class ClientEcho { public static void main(String[] args) { try { // Vérification de la ligne de commande if (args.length != 3) throw new IllegalArgumentException("Erreur de syntaxe"); String serveur = args[0]; int port = Integer.parseInt(args[1]); byte[] requete = args[2].getBytes();
byte[] reponse = new byte[1000]; La même chose en Java... (2/3) // Création d'un socket en mode datagramme DatagramSocket sock = new DatagramSocket(); // Construction de l'adresse de socket du serveur InetAddress adr_serveur = InetAddress.getByName(serveur); // Création du datagramme à envoyer DatagramPacket envoi = new DatagramPacket(requete, requete.length, adr_serveur, port); // Création d'un datagramme "vide" pour recevoir la réponse DatagramPacket recept = new DatagramPacket(reponse, reponse.length); // Envoi du datagramme sock.send(envoi); // Réception de la réponse sock.receive(recept); La même chose en Java... (3/3) // Fermeture de la socket sock.close(); // Affichage de la réponse String mess = new String(reponse, 0, recept.getLength()); System.out.println(recept.getAddress().getHostName() + " : " + mess); } // try catch (Exception e) { System.err.println(e); System.err.println("Usage: java ClientEcho " + "); } // catch } // main } // class La même chose en C#... (1/3) using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; class ClientDate { static void Main(string[] args) { if (args.Length != 2) { Console.Error.WriteLine("Usage: mono client-date.exe "); System.Environment.Exit(1);
} UdpClient sock = new UdpClient(); IPEndPoint IPServeur = new IPEndPoint(IPAddress.Any, 0); La même chose en C#... (2/3) try { sock.Connect(args[0], Int16.Parse(args[1])); while (true) { Console.Write("Entrez une commande : "); String commande = Console.ReadLine(); if (commande.ToUpper().Equals("QUIT")) break; // Conversion en ASCII Byte[] message = Encoding.ASCII.GetBytes(commande); // Envoi de la commande sock.Send(message, message.Length); // Lecture de la rŽponse Byte[] reponse = sock.Receive(ref IPServeur); Console.WriteLine(Encoding.ASCII.GetString(reponse)); } // while } La même chose en C#... (3/3) catch (Exception e) { Console.Error.WriteLine(e.ToString()); } finally { sock.Close(); } } // Main } La même chose en C... (1/3) # ... includes omis... #define TAILLE_TAMPON 1000 static int sock; void ErreurFatale(char message[]) { ... } int main(int argc, char * argv[]) { struct sockaddr_in adresse_socket_serveur; struct hostent *hote; int taille_adresse_socket_serveur, numero_port_serveur, longueur_requete; char *nom_serveur, *requete, reponse[TAILLE_TAMPON]; /* 1. Réception des paramètres de la ligne de commande */ if (argc != 4) { fprintf(stderr, "Usage: %s \n", basename(argv[0])); exit(EXIT_FAILURE); }
La même chose en C... (2/3) nom_serveur = argv[1]; numero_port_serveur = atoi(argv[2]); requete = argv[3]; /* 2. Initialisation de la socket */ /* 2.1 Création de la socket en mode datagramme */ if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) ErreurFatale("Création socket"); /* 2.2 Recherche de la machine serveur */ if (( hote = gethostbyname(nom_serveur)) == NULL) ErreurFatale("Recherche serveur"); /* 2.3 Remplissage adresse serveur */ adresse_socket_serveur.sin_family = AF_INET; adresse_socket_serveur.sin_port = htons(numero_port_serveur); adresse_socket_serveur.sin_addr = *(struct in_addr *)hote->h_addr; taille_adresse_socket_serveur = sizeof(adresse_socket_serveur); La même chose en C... (3/3) /* 3. Envoi de la requête */ longueur_requete = strlen(requete) + 1; printf("REQUETE > %s\n", requete); if (sendto(sock, requete, longueur_requete, 0, /* pas de flags */ (struct sockaddr *)&adresse_socket_serveur, taille_adresse_socket_serveur) < 0) ErreurFatale("Envoi requête"); /* 4. Lecture de la réponse */ if ((recvfrom(sock, reponse, TAILLE_TAMPON, 0, NULL, 0)) < 0) ErreurFatale("Attente réponse"); printf("REPONSE > %s\n",reponse); /* 5. Fermeture de la socket */ close(sock); printf("CLIENT > Fin.\n"); exit(EXIT_SUCCESS); } Communication TCP en Ruby
Algorithme d'un serveur TCP (1/2) Création de la socket. Liaison de la socket à un port d'écoute avec bind. Fixer la taille de la file d'attente avec listen. Se mettre en attente d'une connexion avec accept. Faire un fork ou un thread pour traiter cette requête pendant que le père continue d'attendre une autre connexion. Fermeture de la socket principale à l'arrêt du serveur. Algorithme d'un serveur TCP En Ruby, on utilisera la classe TCPServer qui fournit la méthode accept. Pour créer des connexions filles, on utilisera des instances de la classe Thread. Même remarque sur le schéma : parfois, c'est le serveur qui met fin à la connexion. Exemple : un serveur web (1/6) require 'socket' require 'thread' $repertoire = ARGV[1] # Variable globale def document_non_trouve(fichier, client) client.print
FIN end # document_non_trouve Exemple : un serveur web (2/6) def requete_invalide(client) client.print
FIN desc.each do |ligne| # Remplacement des cars spéciaux ligne.gsub!(/&/, "&") # en premier ! ligne.gsub!(/,/, ">") ligne.gsub!(/\\n/, "\r\n") client.print ligne end # do desc.each Exemple : un serveur web (5/6) client.print STDERR.puts e end # begin
Exemple : un client telnet (1/2) require 'socket' begin # Vérification de la ligne de commande raise "Usage: #{$0} ip [port]\n" if ARGV.length == 0 # Si le port n'est pas indiqué, on prend le port 23 (TELNET) if ARGV[1] port = ARGV[1].to_i else port = 23 end # Création d'une socket TCP sock = TCPSocket.new(ARGV[0], port) Exemple : un client telnet (2/2) Thread.new { # Thread d'écriture begin loop do sock.puts($stdin.gets) # envoi de ce qui est saisi au clavier end rescue Errno::EIO # si le serveur a fermé la connexion sock.close # on sort en fermant aussi end } # fin du thread d'écriture # le processus père lit la réponse du serveur while reponse = sock.gets print reponse end sock.close end Idem, avec gestion du timeout (1/2) require 'socket' require 'timeout' # pour Timeout.timeout require 'english' # pour des constantes "parlantes" begin # pour traiter les exceptions... raise "Usage: #{$0} ip [port]\n" if ARGV.length == 0 # Si le port n'est pas indiqué, on prend le port 23 (TELNET) ARGV[1] = "23" if ! ARGV[1] # Création d'une socket TCP avec un timeout de 5 secondes
# la variable doit être globale (d'où le $) timeout(5) {$sock = TCPSocket.new(ARGV[0], ARGV[1])} rescue RuntimeError => e STDERR.puts e Idem, avec gestion du timeout (2/2) rescue Timeout::Error # déclenché par Timeout.timeout puts "Délai de connexion expiré" rescue # Capture toutes les autres exceptions puts "Pb de connexion : #{$ERROR_INFO}" else # effectué uniquement si pas d'exception # traitement précédent (avec $sock au lieu de sock)... ensure $sock.close if $sock end Exemple d'utilisation de readlines Si on sait qu'une réponse TCP fera plusieurs lignes : >> TCPSocket.open("www.free.fr", "http") do |conn| ?> conn.puts("HEAD / HTTP/1.1") >> conn.puts("Host: localhost") >> conn.puts >> print conn.readlines >> end HTTP/1.1 200 OK Date: Fri, 13 Jan 2006 23:10:54 GMT Server: Apache/1.3.33 (Debian GNU/Linux) Last-Modified: Fri, 13 Jan 2006 23:10:01 GMT ETag: "b5bf-d00c-43c83349" Accept-Ranges: bytes Content-Length: 53260 Connection: close Content-Type: text/html La même chose en Java (1/2) import java.io.*; import java.net.Socket; public class ClientWeb { public static void main(String[] args) { try { // Création d'une socket TCP Socket sock = new Socket("localhost", 80);
// Création des tampons d'E/S BufferedReader du_serveur = new BufferedReader( new InputStreamReader(sock.getInputStream())); PrintWriter au_serveur = new PrintWriter( new OutputStreamWriter(sock.getOutputStream())); La même chose en Java (2/2) // Envoi de la requête HEAD au serveur au_serveur.println("HEAD / HTTP/1.1"); au_serveur.println("Host: localhost\n"); au_serveur.flush(); // Lecture de la réponse du serveur String reponse; while ((reponse = du_serveur.readLine()) != null) System.out.println(reponse); // fermeture de la socket quand le serveur ferme la sienne sock.close(); } catch(Exception e) { System.err.println(e); System.err.println("Usage: java ClientWeb "); } } }
Vous pouvez aussi lire