Programmation des sockets en Ruby - Éric Jacoboni Université du Mirail, IUP NTIE, Toulouse

La page est créée Laura Jacob
 
CONTINUER À LIRE
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