Programmation réseau

 

Rappels

Un réseau est un ensemble de machines et périphériques connectés de manière à pouvoir échanger des données.

Chaque noeud du réseau possède une adresse unique. Une adresse est une quantité numérique mais une correspondance symbolique peut lui être associée.

Le transfert d'information est réalisé par commutation de paquets.

Pour échanger des données, 2 ou plusieurs machines du réseau doivent adhérer à un protocole de communication commun.

Un protocole de communication définit un ensemble de règles qui régissent la communication.

De nombreux protocoles existent. Par exemple, le protocole HTTP règle la communication entre un navigateur web et un serveur. Le protocole SMTP définit la manière dont un courrier électronique est transféré. Tous deux sont des protocoles de haut niveau ( niveau application ). Java n'est pas concerné par les protocoles de bas niveau.

Les réseaux sont organisés en couches du niveau application au niveau physique. Le niveau application est concerné par la délivrance des données à l'utilisateur.

Les applications concernées par ce TP sont de type client/serveur. Le client initie une conversation. Le serveur attend et écoute une requête client.

Protocole IP ( Internet protocol ) : situé sous la couche application, il est chargé du transfert des données entre un client et un serveur sous forme de paquets.

Protocole TCP ( Transmission Control Protocol ) : en conjonction à IP pour contrôler que tous les paquets sont bien arrivés et dans l'ordre d'émission. Il procède à une retransmission en cas de perte.

Protocole UDP ( User Datagram Protocol ) : est un protocole non fiable dans la mesure où aucune garantie de temps de délivrance, d'ordre des paquets et de réception des paquets n'est fournie.

Adresse IP : adresse numérique unique codée sur 4 octets et encapsulée dans les paquets de données. Elle est utilisée par IP pour router les paquets d'une source à une destination. La lecture humaine est facilitée par sa correspondance symbolique. Par exemple, la commande nslookup www.fnac.com fournit le résultat suivant : 195.42.251.40.

DNS ( Domain Name System ) a été développé pour traduire un nom de domaine ( www.fnac.com ) en une adresse IP. En Java, la classe InetAddress permet de récupérer un nom de domaine connaissant son adresse IP.

Un Port joue le rôle d'une sous-adresse logique pour accéder à un serveur particulier. Par analogie, l'adresse IP correspond à un n° de téléphone tandis que le port correspond au n° de poste dans l'entreprise. Théoriquement, 65.535 ports sont disponibles. Les ports 1 à 1023 sont réservés à des services prédéfinis. Par exemple, le port 80 correspond à HTTP, 13 à l'heure et 7 à l'echo.

Une URL ( Uniform Resource Locator ) fournit un accès sur une ressource particulière. Un URL spécifie :

protocol://hostname[:port]/path/filename#ref

Sockets et URL : Java fournit 2 manières de faire de la programmation réseau. La première approche est associée aux classes Socket, DatagramSocket et ServerSocket. La seconde est associée aux classes URL, URLEncoder et URLConnection.

Programmation Socket : la classe Socket permet une programmation réseau utilisant le protocole TCP, la classe DatagramSocket utilise le mode UDP. Toutes deux utilisent la classe ServerSocket.

La programmation socket fournit une approche bas-niveau pour connecter 2 machines. Elle permet l'écoulement des données en mode full-duplex entre client et serveur. Cela ne signifie pas qu'elles sont en mesure de communiquer. Encore faut-il disposer d'un protocole commun. C'est au programmeur d'assigner un protocole acceptable par les 2 parties au niveau application.


Un premier exercice

Il s'agit d'écrire un programme de type client/serveur. Le serveur rend un écho de la chaîne de caractères envoyée par un client précédée de la chaîne : "Voici l'écho : ".

Le serveur d'echo est implémenté par un thread qui gèrer le port 7 ( service standard echo).
Nous nous proposons de créer un service d'écho sur un nouveau port ( n° 7701, 7702, etc ... selon votre poste de travail )

Schéma de la communication client/serveur

Le serveur se met en attente d'une requête client ( méthode accept() ) sur le port 7.
Lorsqu'un appel lui parvient, la méthode accept() retourne une socket qui est passée à un thread (EchoConnection) crée par le serveur pour gérer la communication entre le client et le serveur. Si un autre client émet une requête vers le même serveur, un nouveau thread EchoConnection est crée pour initier une nouvelle connection. Une telle socket est donc associée au port 7. L'identification du client est mémorisée au niveau du thread EchoConnection.

Coté client, une socket est crée vers le serveur avec le n° de port approprié. Cette socket est mise en correspondance à celle du serveur par le biais de l'identifiant du client.

Marche à suivre

On procédera progressivement .

Gestion de la sécurité

Par défaut, une application Java ne gère pas la sécurité. Aucune restriction n'est prévue sur ce que peut faire le programme.
Pour établir une gestion de la sécurité, on envoie le message setSecurityManager au gestionnaire de sécurité, ce qui a pour effet d'interdire tout privilège au code.
Pour personnaliser le gestionnaire de sécurité, il faut étendre la classe SecurityManager.
Les méthodes commençant par check contrôlent l'accès aux différents privilèges.
La version par défaut de chaque méthode rend le privilège associé indisponible.
Pour rendre le privilège disponible, il faut redéfinir la méthode avec une méthode vide
Pour personnaliser un privilège, il faut la redéfinir suivant le besoin.

Pour l'exercice, nous étendons la classe SecurityManager de manière à donner tous les privilèges. Pour cela chaque méthode commencant par check est redéfinie avec un corps vide.

code de la classe ServeurSecurityManager.

Le serveur d'écho : EchoServeur

Son rôle est d'instancier un serveur de socket (classe java ServerSocket) sur le port 77xx capable de générer une socket dès qu'un appel client survient.
L'EchoServeur est un thread qui boucle indéfiniment en attente de requête client.
Le premier appel en provenance du client déclenche un accept() et par suite la création d'un thread EchoConnection qui gère la connection avec ce client via une socket coté serveur. Cette socket est représentée sur le schéma ci-dessus par 2 flots, l'un en entrée et l'autre en sortie. L'échange d'information consiste donc à lire ou écrire sur ces flots.

code de la classe EchoServeur

Le gestionnaire de connection : EchoConnection

L'objet qui gère de bout en bout la connection est un thread attaché à une socket liée au port 77xx.
Rendu actif (méthode run()), ce thread crée un flot en lecture (inputStream) et un flot en écriture (outputStream), tous deux attachés à la socket.

Le gestionnaire de connection se contente de lire une chaîne input de caractères sur le flot inputStream et d'écrire la chaîne "Voici l'écho : "+input sur outputStream.

code de la classe EchoConnection

Un Client : EchoClient

Connaissant l'adresse IP du serveur et le port sur lequel le service convoité est installé, le client crée une socket qu'il associe à un flot en lecture (inputStream) et un flot en écriture (outputStream).

Le client écrit la chaîne "Ceci est un test" sur outputStream puis lit l'écho sur inputStream.

Lorsque la communication est terminée chaque socket doit être fermée.

code de la classe EchoClient

 

Production du programme serveur sur la machine joule.cnam.fr

public class AppliEcho
{
  public static void main(String[] args)
  {
// Création d'un gestionnaire de sécurité personnalisé
    System.setSecurityManager(new ServeurSecurityManager());
// Instantiation d'un serveur d'objets sur le port 77xx
// Renvoie en echo les chaînes reçues de clients TCP/IP

    EchoServeur echoServeurThread = new EchoServeur();
  }
}

 

Production du programme client sur la machine newton.cnam.fr


Un second exercice

On souhaite écrire un serveur qui prenne en charge des requêtes client selon le protocole HTTP.
Pour simplifier, ce serveur ne se préoccupera que de la commande GET du protocole. Les autres commandes seront ignorées. Un message d'erreur sera envoyé au client si elles sont employées.

Implantation du serveur HTTP

Le code du serveur HTTP est identique à celui du serveur d'écho. La seule différence est qu'il est attaché au port 80xx. Ce serveur a pour tâche de créer une connection HTTP dès qu'une requête client survient en lui passant une socket qui est connectée à la socket client.

Le code de la classe HTTPConnection est semblable à celui de la classe EchoConnection pour ce qui concerne son constructeur. La différence essentielle porte sur la méthode run()où le principal travail est réalisé.

Implantation de la méthode  run()



Complétez le code suivant :


public void run()
{
  System.out.println("thread actif sur port 80");
  try
  {
// Création d'un flot en entrée à partir de la socket
    ............................................
// Création d'un flot en sortie vers la socket
// pour envoyer des chaînes de caractères à un client.

    ............................................
// Création d'un flot en sortie vers la socket
// pour envoyer des tableaux de bytes à un client.

    byteOutput = new DataOutputStream(socket.getOutputStream());
// Lecture d'une requête à partir du navigateur
    ............................................
    System.out.println("Requête lue : " + requete);
// Analyse et tentative de réponse à la requête
// requête uniquement de type GET
// les autres types de requêtes sont ignorées

    StringTokenizer stringTokenizer = new StringTokenizer(requete);
    if((stringTokenizer.countTokens() >= 2) && stringTokenizer.nextToken().equals("GET"))
    {
      System.out.println("Le premier token est : GET");
      requete = stringTokenizer.nextToken());
      if(requete.endsWith("/") || requete.equals(""))
     
{
        System.out.println("Requete se terminant par / ou espace ");
        System.out.println(" concaténée avec index.html");
        ............................................
        System.out.println("Requête modifiée : " + requete);
      }
      System.out.println("Lecture du fichier : " + requete);
      FileInputStream fileInputStream = new FileInputStream(requete);
// Déclaration d'une variable capable de recevoir les octets
// du fichier. Sa taille est égale au nombre d'octets pouvant
// être lus sans blocage.

      byte[] data = new byte[fileInputStream.available()];
// Lecture du fichier

      ............................................
// Envoi du tableau de bytes au client.
      ............................................
      byteOutput.flush();
    }//end if stringTokenizer.countTokens...
    else
// Lorsque la requête n'est pas GET
      outputStream.println("<HTML><BODY><P> 400 Requête non interprétée <P></BODY></HTML>");
    System.out.println("Socket fermée");
    ............................................
  }
  catch(FileNotFoundException e){
    outputStream.println("<HTML><BODY><P>404 Fichier non trouvé<P></BODY></HTML>");
    try{
      socket.close();
      System.out.println("Socket fermée");
    }catch(IOException evt){System.out.println(evt);}
  }//fin catch FileNotFoundException

  catch(SecurityException e){
    System.out.println(e);
    e.printStackTrace();
    outputStream.println("<HTML><BODY><P>403 Opération interdite<P></BODY></HTML>");
    outputStream.println(e);
    try{
      socket.close();
      System.out.println("Socket fermée");
    }catch(IOException evt){System.out.println(evt);}
  }//fin catch SecurityException

  catch( IOException e){
    System.out.println( "Erreur d'E/S " + e );
    try{
      socket.close();
      System.out.println("Socket fermée");
    }catch(IOException evt){System.out.println(evt);}
  }//fin catch IOException
}//fin méthode run

A ce stade, le serveur HTTP peut être lancé sur la machine joule.cnam.fr.
Il faut, pour la poursuite du programme céer 2 fichiers l'un nommé index.html dans le répertoire du serveur, l'autre dans le répertoire père.

Implantation du client

Syntaxe simplifiée de la commande GET :

GET nom_du_fichier

import java.net.*;
import java.io.*;

class HTTPClient
{
  public static void main(String[] args)
  {
    String serveur = "joule.cnam.fr";
    int port = 8080;
    try{
// Création d'une socket, connectée au serveur HTTP
// sur le port spécifié.

      .......................................
// Création d'un flot d'entrée à partir de la socket
      
.......................................
// Création d'un flot de sortie sur la socket
     
.......................................
// Envoi d'une commande GET au serveur
// le fichier requis appartient au répertoire père

      .......................................
// le fichier requis est le fichier index.html du répertoire courant
            .......................................
      String ligne = null;
// Boucle de lecture et d'affichage à l'écran tant que null n'est pas reçu
      .......................................

      .......................................
// Fermer la socket
      .......................................
    }//fin try
    catch(UnknownHostException e){
      System.out.println(e);
      System.out.println("l'hôte n'est pas activé");
    }//fin catch UnknownHostException
  catch(IOException e){System.out.println(e);}
  }
}

Lancer ce programme sur la machine newton.cnam.fr


Un troisième exercice

 

Il s'agit de reprendre une partie du projet gestion des comptes bancaires. Un compte est une instance de la classe  Compte :

    public class Compte{
       private String numero;
       private float solde;
       public Compte( String numero, float solde ){  
             this.numero = numero;
             this.solde = solde;
       }
       public void crediter( float montant ){
             solde = solde + montant;
       }
       public void debiter( float montant ){
             solde = solde - montant;
       }
       public float quelSolde(){ return solde; }
       // on se limite à ces méthodes
   }

 On considère un serveur capable de gérer un compte courant. Son titulaire peut l'interroger et/ou le débiter à distance.

On demande d'écrire le programme en posant que le compte est crée avec un numéro fixé initialement ainsi qu'un solde quelconque. Le client se contentera d'interroger le compte puis de le débiter d'une somme quelconque.