• 6 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 06/02/2020

Les sockets : côté client

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Maintenant vous le savez, pour faire communiquer deux machines sur le réseau nous allons devoir utiliser des adresses, des ports et des protocoles mais pour que celles-ci puissent s'échanger des informations, les deux parties de la communication doivent utiliser des sockets.
La plupart des systèmes d'exploitation proposent une abstraction pour manipuler les données réseaux, et ce sont ces fameuses sockets.

Mais qu'est-ce qu'une socket, concrètement ?

En fait, une socket est un moyen de partager des données en réseau et elle se manipule comme un fichier. Si vous savez manipuler des fichiers en Java, vous saurez rapidement manipuler des sockets. :)

Par contre, Java fait la différence entre deux types de sockets :

  • Socket côté client : permet de se connecter à une machine distante afin de communiquer avec elle ;

  • Socket côté serveur : connexion qui attend qu'un client vienne se connecter afin de communiquer avec lui.

Nous allons commencer par nous intéresser aux sockets côté client dans ce chapitre. Vous verrez, il y a pas mal de choses à retenir !

Généralité sur les sockets

Les données transmises sur le réseau sont structurées et encapsulées dans ce qu'on appelle des paquets, ou datagrammes. Chaque datagramme contient une section en-tête et un corps. L'en-tête contient toutes les informations nécessaires au transport de l'information, et le corps contient les données en transit.

Ils doivent être énormes ces paquets !

Et non, en fait, lorsque vous recherchez une page web sur Internet, avant que celle-ci ne s'affiche, une multitude de paquets sont échangés car les données sont divisées en plusieurs morceaux et réassemblés à l'arrivée pour être utilisées : rappelez-vous les petits schémas sur les protocoles TCP et UDP…

Les sockets vous permettent de ne pas avoir à faire ce travail de découpage vous-mêmes, c'est génial ! Elles vous permettent de traiter n'importe quelle communication réseau comme si vous lisiez ou écriviez dans un fichier. En fait, elles vous permettent de :

  • Vous connecter à une machine distante ;

  • Recevoir et envoyer des données ;

  • Fermer une connexion établie ;

  • Attendre une connexion de l'extérieur ;

  • Écouter les communications entrantes ;

  • Utiliser un port.

Les sockets côté serveur peuvent faire appel à toutes ces fonctionnalités - on en parlera dans la prochaine partie. En revanche, les sockets côté client n'utiliseront que les trois premières fonctionnalités.

L'utilisation de socket côté client revient quasiment toujours à faire ceci (dans l'ordre) :

  1. Instancier une socket ;

  2. Connecter la socket avec le service côté serveur ciblé (FTP, HTTP…) ;

  3. Échanger les données avec la socket serveur (par contre, le format et les commandes envoyées au serveur peuvent changer en fonction du protocole utilisé : on n’utilise pas un serveur FTP comme un serveur HTTP) ;

  4. Fermer la socket lorsque les communications sont terminées.

Le côté magique des sockets est que les communication sont dites en full-duplex, c'est-à-dire que le client et le serveur peuvent envoyer et recevoir des signaux simultanément, donc il n'y a aucune attente.

La classe Socket

La classejava.net.Socketest l'élément fondamental de toute connexion réseau côté client.
Elle utilise par défaut le protocole TCP et bénéficie de deux constructeurs vous permettant de l'instancier avec soit un objetInetAddresssoit un objetString, représentant le nom de l'hôte serveur avec lequel vous souhaitez communiquer. Ces deux constructeurs prennent un deuxième paramètre correspondant au numéro de port (de typeint) que vous souhaitez utiliser pour établir la connexion avec le serveur.

Voici un code vous montrant ces syntaxes :

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

public class TestSocket {

   public static void main(String[] args) {
      try {
         Socket soc1 = new Socket(InetAddress.getByName("www.openclassrooms.com"), 80);
         Socket soc2 = new Socket("www.openclassrooms.com", 80);
         Socket soc3 = new Socket("toto", 80);
      } catch (UnknownHostException e) {
         e.printStackTrace();
      }catch (IOException e) {
         e.printStackTrace();
      }
   }
}

La troisième socket lève une exception de type UnknownHostExceptioncar "toto" n'est pas connu. D'autres exceptions peuvent être levées en cas d'échec de connexion pour les raisons suivantes :

  • L'hôte n'accepte pas les connexions ;

  • L'hôte n'accepte pas les connexions sur le port demandé ;

  • Un problème de réseau ;

Vous aurez donc compris que cet objet ne fait pas que créer une socket, il tente également de rentrer en communication avec l'hôte distant pour s'assurer que celui-ci est d'accord pour communiquer. De ce fait, il est facile de faire un petit programme qui permet de savoir quels ports sont ouverts et qui accepte les connexions sur un hôte. Le code suivant teste les 1024 premiers ports de ma machine et m'informe des ports ouverts :

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

public class TestSocket {

   public static void main(String[] args) {
      for(int i = 1; i <= 1024; i++){
         try {
             Socket soc = new Socket("127.0.0.1", i);
             System.out.println("La machine autorise les connexions sur le port : " + i);
         } catch (UnknownHostException e) {
            e.printStackTrace();
         }catch (IOException e) {
            //Si une exception de ce type est levée
            //c'est que le port n'est pas ouvert ou n'est pas autorisé
         }
      }
   }
}

Ce qui me donne ce résultat :

Première utilisation de Socket
Première utilisation de Socket

Je vous avais dit un peu plus tôt qu'il est possible qu'une machine possède plusieurs interfaces réseaux. Ces interfaces peuvent être configurées pour accepter et/ou refuser certains types de connexions. Par conséquent, il est possible que vous soyez obligé de spécifier non seulement l'adresse et le port du serveur auquel vous souhaitez vous connecter mais aussi sur quelle interface et quel port le serveur devra répondre. La classeSocketpossède d'autres constructeurs qui permettent de faire cela, en voici un exemple :

import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;


public class Interface {

   public static void main(String[] args) {
      
      try {         
         //Nous spécifions que la réponse devra se faire par ce chemin
         InetAddress lo = InetAddress.getByName("192.168.2.44");
         
         //Le fait de mettre 0 dans le numéro de port de réponse
         //informe que n'importe quel numéro est accepté
         Socket soc = new Socket("www.openclassrooms.com", 80, lo, 0);         
      }catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

Ma machine ayant plusieurs interfaces réseaux, et donc plusieurs adresses IP, il se peut que le trafic réseau soit bridé sur une ou plusieurs adresses. Je sais de source sûre que l'adresse 192.168.2.44 autorise des connexions de l'extérieur, je spécifie donc cette adresse comme adresse de réponse au serveur.

Il existe encore une autre façon de se connecter à un serveur : en utilisant un proxy.

Un quoi ?

Un proxy. Pour simplifier au maximum, considérez un proxy comme un médiateur ou un intermédiaire. C'est lui qui fait le lien entre le client et le serveur. en mode normal, le client et le serveur conversent directement, avec un proxy, le client et le serveur passent par cet intermédiaire et c'est lui qui retourne les conversations aux interlocuteurs. voici un schéma représentant ce mode de fonctionnement :

Utilisation d'un proxy
Utilisation d'un proxy

La classeSocket vous permet aussi d'initialiser ce type de connexion mais d'autres objets entrent en jeu. Voici un exemple simple pour illustrer mes dires :

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;


public class TestProxy {

   public static void main(String[] args) {
      
      //On crée une adresse correspondant à notre proxy (fictif)
      SocketAddress proxyAddress = new InetSocketAddress("10.10.10.10", 8080);
      
      //On instancie la classe Proxy avec le type souhaité
      Proxy proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
      
      //On crée notre socket utilisant le proxy
      Socket s = new Socket(proxy);
      
      //On crée l'adresse que l'on souhaite atteindre via le proxy
      SocketAddress remote = new InetSocketAddress("www.openclassrooms.com", 80);
      try {
         //On connecte le tout !
         s.connect(remote);
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

À quoi correspond la constanteProxy.Type.SOCKS?

SOCKS est l'acronyme de Secured Over Credential-based Kerberos.
Elle correspond au type de proxy que vous souhaitez utiliser. Il en existe plusieurs sorte mais celui-ci représente un proxy standard que vous pourriez utiliser si vous êtes dans une structure, protégée par un firewall, et que vous souhaitez accéder à des ressources extérieures. Dans ce cas, il y a fort à parier que ce soit ce type de proxy que vous utiliserez.
Il existe encore deux constantes utilisables :

  • HTTP: correspond à un proxy HTTP ;

  • DIRECT: qui signifie qu'il n'y a pas de proxy à utiliser.

Voilà pour la façon de créer une socket. Maintenant, faisons un peu le tour des méthodes utiles de cet objet, voici un code qui en utilise quelques unes :

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


public class SocketMethods {

   public static void main(String[] args) {
      try {
         Socket s = new Socket("www.openclassrooms.com", 80);
         System.out.println("Port de communication côté serveur : " + s.getPort());
         System.out.println("Port de communication côté client : " + s.getLocalPort());
         System.out.println("Nom de l'hôte distant : " + s.getInetAddress().getHostName());
         System.out.println("Adresse de l'hôte distant : " + s.getInetAddress().getHostAddress());
         System.out.println("Adresse socket de l'hôte distant : " + s.getRemoteSocketAddress());
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

Et voici ce que nous obtenons :

Informations sur les sockets
Informations sur les sockets

Une dernière chose avant de poursuivre : les sockets sont intelligentes et se ferment toutes seules, lorsqu'un programme ou qu'un des côtés de la communication se ferme. Cependant, je vous déconseille fortement de laisser le programme gérer ce genre de fermeture. Rappelez-vous que les sockets fonctionnent de la même façon qu'un flux ouvert vers un fichier. De ce fait, la fermeture de ce genre de flux est régie par les mêmes règles. Voici comment vous assurer qu’une socket est fermée correctement :

public class CloseSocket {

   public static void main(String[] args){
      Socket sock = null;
      try {
         //On se connecte à OpenClassrooms
         sock = new Socket("www.openclassrooms.com", 80);
         
         //...         
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
      finally{
         if(sock != null){
            try {
               sock.close();
            } catch (IOException e) {
               e.printStackTrace();
               sock = null;
            }
         }
      }
   }
}

Vous êtes familier de ce genre de choses, donc rien de particulier à souligner ici. :)

Voilà, nous en avons fini pour les présentations des sockets côté client. Il ne nous reste plus qu’à les utiliser pour communiquer avec un serveur. :-°

Communiquer avec un serveur

Maintenant que vous savez comment créer une socket client, nous allons voir comment dialoguer avec un serveur. Nous avons déjà vu comment dialoguer avec un serveur web via l'objetHttpURLConnection, je vous propose de faire de même avec l'objetSocket. :)

En guise d’exemple, nous allons essayer de nous connecter à Wikipédia et d'afficher une page.

package Communication;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class SdzConnection {

   public static void main(String[] args){
      try {
         //On se connecte à OC
         Socket sock = new Socket("fr.wikipedia.org", 80);
         
         //On récupère maintenant la réponse du serveur 
         //dans un flux, comme pour les fichiers...
         BufferedInputStream bis = new BufferedInputStream(sock.getInputStream());
         
         //Il ne nous reste plus qu'à le lire
         String content = "";
         int stream;
         while((stream = bis.read()) != -1){
            content += (char)stream;
         }
         
         Browser browser = new Browser("fr.wikipedia.org", content);
         
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }   
}

Ce qui nous donne... rien du tout !

Qu'est-ce qu'il se passe ? o_O

C'est simple, nous nous sommes bien connectés au serveur, mais nous ne lui avons rien demandé, donc il ne nous répond rien et notre socket est en attente.
Lorsque notre programme atteint la ligne où nous lisons dans le flux d'entrée, le thread où se lance cette tâche se met en attente car le serveur n'a rien renvoyé !

Pour pouvoir demander une page à un serveur web, nous devons respecter son mode de communication et envoyer une demande au serveur.
Pour demander une page à un serveur web, vous connaissez la commande :GET. Mais pour avoir tout ce dont nous allons avoir besoin, nous pouvons utiliser un plugin Firefox qui se nomme Live HTTP Header et qui permet de capturer toutes les en-têtes (les commandes) envoyées au serveur. Ce plugin est disponible sur Firefox et sur Chrome.

Je vous invite donc à l'installer en allant dans "Outils/Modules Complémentaires"
Dans le filtre de recherche, tapez "Http header" et vous devriez avoir le module qui vous est proposé.

Une fois le plugin installé, rendez-vous sur la page d'accueil de Wikipédia. Maintenant, allez dans le menu "Outils/En-têtes Http en direct" :

Image utilisateur

Une popup s'ouvre ressemblant à ceci :

Image utilisateur

La case à cocher signifie que la popup capture toutes les en-têtes HTTP envoyées par votre navigateur.

La popup étant maintenant ouverte, rafraîchissez la page d'accueil de Wikipédia et vous devriez avoir quelque chose comme ceci :

Entête HTTP
Entête HTTP

Maintenant nous savons ce que nous devons envoyer au serveur, voici donc le code complété :

package Communication;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class SdzConnection {

   public static void main(String[] args){
      try {
         //On se connecte à Wikipedia
         Socket sock = new Socket("fr.wikipedia.org", 80);
         
         //Nous allons faire une demande au serveur web
         //ATTENTION : les \r\n sont OBLIGATOIRES ! Sinon ça ne fonctionnera pas ! ! 
         String request = "GET /wiki/Digital_Learning HTTP/1.1\r\n";
         request += "Host: fr.wikipedia.org\r\n";
         request += "\r\n";
                 
         //nous créons donc un flux en écriture
         BufferedOutputStream bos = new BufferedOutputStream(sock.getOutputStream());
         
         //nous écrivons notre requête
         bos.write(request.getBytes());
         //Vu que nous utilisons un buffer, nous devons utiliser la méthode flush
         //afin que les données soient bien écrite et envoyées au serveur
         bos.flush();
         
         //On récupère maintenant la réponse du serveur 
         //dans un flux, comme pour les fichiers...
         BufferedInputStream bis = new BufferedInputStream(sock.getInputStream());
         
         //Il ne nous reste plus qu'à le lire
         String content = "";
         int stream;
         byte[] b = new byte[1024];
         while((stream = bis.read(b)) != -1){
            content += new String(b, 0, stream);
         }
        
         //On affiche la page ! 
         Browser browser = new Browser("fr.wikipedia.org", content);        
         
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }   
}

Et nous arrivons à afficher la page parlant de Digital Learning sur Wikipédia ! Victoire !
Vous voyez, c'est très simple. La seule difficulté est de trouver la bonne syntaxe pour la communication avec le serveur. Ici, nous avons utilisé des commandes HTTP, mais nous aurions pu coder un client FTP rudimentaire et nous aurions dû, dans ce cas, utiliser des commandes FTP. ;)

Tiens ! Ca ferait un bon TP…

Et bien justement, nous allons en faire un dans le chapitre suivant. Maintenant que vous savez comment initialiser une socket client et communiquer avec un serveur web, il n’a rien de tel qu’un peu de pratique pour vous roder !

Exemple de certificat de réussite
Exemple de certificat de réussite