• 6 hours
  • Hard

Free online content available in this course.

course.header.alt.is_certifying

Got it!

Last updated on 2/6/20

Les sockets : côté serveur

Log in or subscribe for free to enjoy all this course has to offer!

Nous avons vu précédemment comment initialiser une connexion d'un client vers un serveur. Le moment est maintenant venu de mettre les mains un peu plus dans le cambouis… Nous allons voir comment créer une connexion côté serveur et vous verrez que c'est un travail très différent !

En fait, pour vous brosser rapidement le tableau, dîtes-vous qu'un serveur ne sait pas à l'avance quel client va s'y connecter pour dialoguer avec lui, ni quand il va le faire. Pour faire une analogie simple, imaginez une réceptionniste, assise sur sa chaise en attendant que quelqu'un passe la porte pour lui demander quelque chose : elle ne sait ni qui, ni quand quelqu'un va entrer mais elle se doit d'être présente et opérationnelle.

Java met à disposition une classe spéciale permettant de coder des applications serveurs, la classe ServerSocket. Nous allons donc étudier cette classe, son fonctionnement et mettre en place notre premier serveur applicatif en Java. :)

La classe ServerSocket : présentation

Cette classe offre tout le nécessaire aux développeurs Java afin de créer et mettre à disposition une application serveur en Java. Elle possède un constructeur permettant de spécifier le port d'écoute du serveur et une méthode qui permet d'attendre une connexion cliente. En bref, voici comment vont se passer les choses dans notre serveur :

  1. Une nouvelle ServerSocket est créée sur le port spécifié ;

  2. Cette socket attend une connexion cliente (grâce à sa méthode accept()) ;

  3. Selon ce que votre serveur fait, vous pourrez invoquer les méthodes getInputStream() ou getOutputStream() depuis la connexion cliente afin de travailler avec votre serveur ;

  4. Le client et le serveur interagissent ensemble;

  5. Le client et/ou le serveur mettent fin à la connexion en cours ;

  6. La socket serveur retourne à l'étape N°2.

Maintenant, voyons comment construire et initialiser une socket serveur, le code ci-dessous va tenter de créer une socket serveur sur tous les ports existants et va retourner une erreur sur les ports déjà utilisés :

import java.io.IOException;
import java.net.ServerSocket;

public class ServerSocketConstructor {

   public static void main(String[] args) {

      for(int port = 1; port <= 65535; port++){
         try {
            ServerSocket sSocket = new ServerSocket(port);
         } catch (IOException e) {
            System.err.println("Le port " + port + " est déjà utilisé ! ");
         }
      }
   }
}

Ce qui me donne :

Erreurs signalées sur les ports déjà utilisés
Erreurs signalées sur les ports déjà utilisés

Certains ports étant déjà utilisés par d'autres programmes, nous ne pouvons pas créer de socket sur ceux-ci, une exception est donc levée lors de l'instanciation de notre objet.

C'est bizarre, tu n'as pas spécifié d'adresse de connexion au serveur… C'est normal ?

Avec ce constructeur, oui, c'est tout à fait normal. Vous savez maintenant qu'un poste peut avoir plusieurs adresses IP et même plusieurs cartes réseaux. Avec ce constructeur, votre socket serveur va écouter le port que vous avez défini sur toutes les adresses IP de la machine, mais vous avez aussi un autre constructeur qui prend plus de paramètres, comme ceci :

String IP = "192.168.10.10";
int fileAttente = 100;
ServerSocket sSocket = new ServerSocket(port, fileAttente, InetAddress.getByName(IP));

Vous connaissez déjà le premier paramètre et vous avez deviné à quoi va servir le dernier, c'est l'adresse IP que va écouter notre socket. Celui du milieu est un nombre limite de sockets client que notre serveur peut tolérer avant de rejeter les demandes. En fait, il faut bien avoir conscience que notre serveur ne va pas traiter une demande à la fois, il devra pouvoir traiter des demandes en parallèle, donc dans des threads afin de pouvoir fournir un service digne de ce nom…
Imaginez seulement que les serveurs web ne puissent traiter qu'une demande à la fois, il faudra donc attendre que toutes les personnes qui ont demandé une page avant nous soient traitées avant que notre tour vienne… Pas terrible comme fonctionnement. Et bien cet entier sert à donner une limite de tentative de connexion depuis des postes clients. Par défaut, la plupart des systèmes d'exploitation fixent cette limite à 5 socket clients par socket serveur, mais grâce à ce paramètre, nous pouvons augmenter ce nombre.

Exemple de serveur multithreads

Après cette présentation, je me propose de vous donner un exemple de serveur travaillant en mode multithread afin de pouvoir gérer plusieurs connexions clientes à la fois sans bloquer tous les interlocuteurs.
Ce que nous allons faire, c'est un simple serveur de temps : un serveur qui va vous donner l'heure courante, la date, ou les deux, ceci grâce à une commande que vous lui fournirez… vous verrez, ce code est très simple à comprendre et n'est pas très différent du code précédent.

Code côté serveur

Voici le détail des codes que notre serveur va utiliser, l’un pour initialiser le serveur, et l’autre pour initialiser la classe qui traitera les demandes des clients dans un thread séparé.

TimeServer.java
package Timer;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class TimeServer {

   //On initialise des valeurs par défaut
   private int port = 2345;
   private String host = "127.0.0.1";
   private ServerSocket server = null;
   private boolean isRunning = true;
   
   public TimeServer(){
      try {
         server = new ServerSocket(port, 100, InetAddress.getByName(host));
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   
   public TimeServer(String pHost, int pPort){
      host = pHost;
      port = pPort;
      try {
         server = new ServerSocket(port, 100, InetAddress.getByName(host));
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   
   
   //On lance notre serveur
   public void open(){
      
      //Toujours dans un thread à part vu qu'il est dans une boucle infinie
      Thread t = new Thread(new Runnable(){
         public void run(){
            while(isRunning == true){
               
               try {
                  //On attend une connexion d'un client
                  Socket client = server.accept();
                  
                  //Une fois reçue, on la traite dans un thread séparé
                  System.out.println("Connexion cliente reçue.");                  
                  Thread t = new Thread(new ClientProcessor(client));
                  t.start();
                  
               } catch (IOException e) {
                  e.printStackTrace();
               }
            }
            
            try {
               server.close();
            } catch (IOException e) {
               e.printStackTrace();
               server = null;
            }
         }
      });
      
      t.start();
   }
   
   public void close(){
      isRunning = false;
   }   
}
ClientProcessor.java
package Timer;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.text.DateFormat;
import java.util.Date;

public class ClientProcessor implements Runnable{

   private Socket sock;
   private PrintWriter writer = null;
   private BufferedInputStream reader = null;
   
   public ClientProcessor(Socket pSock){
      sock = pSock;
   }
   
   //Le traitement lancé dans un thread séparé
   public void run(){
      System.err.println("Lancement du traitement de la connexion cliente");

      boolean closeConnexion = false;
      //tant que la connexion est active, on traite les demandes
      while(!sock.isClosed()){
         
         try {
            
            //Ici, nous n'utilisons pas les mêmes objets que précédemment
            //Je vous expliquerai pourquoi ensuite
            writer = new PrintWriter(sock.getOutputStream());
            reader = new BufferedInputStream(sock.getInputStream());
            
            //On attend la demande du client            
            String response = read();
            InetSocketAddress remote = (InetSocketAddress)sock.getRemoteSocketAddress();
            
            //On affiche quelques infos, pour le débuggage
            String debug = "";
            debug = "Thread : " + Thread.currentThread().getName() + ". ";
            debug += "Demande de l'adresse : " + remote.getAddress().getHostAddress() +".";
            debug += " Sur le port : " + remote.getPort() + ".\n";
            debug += "\t -> Commande reçue : " + response + "\n";
            System.err.println("\n" + debug);
            
            //On traite la demande du client en fonction de la commande envoyée
            String toSend = "";
            
            switch(response.toUpperCase()){
               case "FULL":
                  toSend = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.MEDIUM).format(new Date());
                  break;
               case "DATE":
                  toSend = DateFormat.getDateInstance(DateFormat.FULL).format(new Date());
                  break;
               case "HOUR":
                  toSend = DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date());
                  break;
               case "CLOSE":
                  toSend = "Communication terminée"; 
                  closeConnexion = true;
                  break;
               default : 
                  toSend = "Commande inconnu !";                     
                  break;
            }
            
            //On envoie la réponse au client
            writer.write(toSend);
            //Il FAUT IMPERATIVEMENT UTILISER flush()
            //Sinon les données ne seront pas transmises au client
            //et il attendra indéfiniment
            writer.flush();
            
            if(closeConnexion){
               System.err.println("COMMANDE CLOSE DETECTEE ! ");
               writer = null;
               reader = null;
               sock.close();
               break;
            }
         }catch(SocketException e){
            System.err.println("LA CONNEXION A ETE INTERROMPUE ! ");
            break;
         } catch (IOException e) {
            e.printStackTrace();
         }         
      }
   }
   
   //La méthode que nous utilisons pour lire les réponses
   private String read() throws IOException{      
      String response = "";
      int stream;
      byte[] b = new byte[4096];
      stream = reader.read(b);
      response = new String(b, 0, stream);
      return response;
   }
   
}

Code côté client

Maintenant, voici les classes que j'utilise côté client.

ClientConnexion.java
package Timer;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Random;

public class ClientConnexion implements Runnable{

   private Socket connexion = null;
   private PrintWriter writer = null;
   private BufferedInputStream reader = null;
   
   //Notre liste de commandes. Le serveur nous répondra différemment selon la commande utilisée.
   private String[] listCommands = {"FULL", "DATE", "HOUR", "NONE"};
   private static int count = 0;
   private String name = "Client-";   
   
   public ClientConnexion(String host, int port){
      name += ++count;
      try {
         connexion = new Socket(host, port);
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   
   
   public void run(){

      //nous n'allons faire que 10 demandes par thread...
      for(int i =0; i < 10; i++){
         try {
            Thread.currentThread().sleep(1000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         try {

            
            writer = new PrintWriter(connexion.getOutputStream(), true);
            reader = new BufferedInputStream(connexion.getInputStream());
            //On envoie la commande au serveur
            
            String commande = getCommand();
            writer.write(commande);
            //TOUJOURS UTILISER flush() POUR ENVOYER RÉELLEMENT DES INFOS AU SERVEUR
            writer.flush();  
            
            System.out.println("Commande " + commande + " envoyée au serveur");
            
            //On attend la réponse
            String response = read();
            System.out.println("\t * " + name + " : Réponse reçue " + response);
            
         } catch (IOException e1) {
            e1.printStackTrace();
         }
         
         try {
            Thread.currentThread().sleep(1000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
      
      writer.write("CLOSE");
      writer.flush();
      writer.close();
   }
   
   //Méthode qui permet d'envoyer des commandeS de façon aléatoire
   private String getCommand(){
      Random rand = new Random();
      return listCommands[rand.nextInt(listCommands.length)];
   }
   
   //Méthode pour lire les réponses du serveur
   private String read() throws IOException{      
      String response = "";
      int stream;<question></question>
      byte[] b = new byte[4096];
      stream = reader.read(b);
      response = new String(b, 0, stream);      
      return response;
   }   
}
Main.java
package Timer;
public class Main {

   public static void main(String[] args) {
    
      String host = "127.0.0.1";
      int port = 2345;
      
      TimeServer ts = new TimeServer(host, port);
      ts.open();
      
      System.out.println("Serveur initialisé.");
      
      for(int i = 0; i < 5; i++){
         Thread t = new Thread(new ClientConnexion(host, port));
         t.start();
      }
   }
}

Et voilà un extrait de ce que j'obtiens :

Requêtes de notre serveur de temps en mode multithread
Requêtes de notre serveur de temps en mode multithread

Vous pourrez voir que les différentes couleurs représentent les requêtes et les réponses reçues. Alors, vous voyez, la programmation réseau n'est pas très compliquée !

Dans ton code, tu nous dis que les objets utilisés ne sont pas les mêmes que d'habitude. Pourquoi ça ?

La réponse est simple. Je vous ai aussi dit qu'il faut impérativement utiliser la méthode flush() pour que les données soient envoyées vers leurs destinataires. Dans nos codes précédents, le client ne faisait qu'une requête vers le serveur et le serveur ne faisait donc qu'une seule réponse : nous fermions alors nos flux avec la méthode close(), ce qui a pour effet de vider le tampon et d'écrire réellement sur le flux avant de fermer le flux, le destinataire reçoit donc les infos. Mais dans ce cas-ci, nous conservons une connexion afin d'envoyer plusieurs requêtes et nous ne devons pas fermer notre flux : je vous rappelle que la fermeture du flux revient à fermer la connexion entre le client et le serveur et nous ne voulons pas ça ! Nous devons donc utiliser la méthode flush() afin d'écrire sur le flux sans le fermer.

D'accord, mais pourquoi changer d'objet ? Les objets que nous utilisions depuis le début ont aussi une méthode flush()

Tout simplement car tous les objets traitant des flux n'ont pas une méthode flush() effective, c'est-à-dire que certains objets (comme ceux que nous utilisions depuis le début) ont bien une méthode flush() disponible, mais celle-ci ne fait rien du tout. Donc dans notre cas, elle ne sert à rien et les connexions attendent sans cesse une réponse.

Nous venons de voir comment mettre en place une connexion serveur avec un traitement en mode multithread, et comme vous aurez pu le constater, il n'y a rien de difficile là-dedans.
Cependant, toutes les communications que vous avez faites jusqu'à maintenant utilisaient le protocole TCP, et comme je vous l'avais dit, il existe un autre protocole de communication, moins sûr mais plus rapide, le protocole UDP. C'est ce que nous allons voir tout de suite !

Example of certificate of achievement
Example of certificate of achievement