• 6 hours
  • Hard

Free online content available in this course.

course.header.alt.is_certifying

Got it!

Last updated on 2/6/20

Communication réseau avec le protocole UDP

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

Dans les chapitres précédents, nous avons utilisé le protocole TCP pour communiquer sur le réseau. Nous avons vu précédemment que ce protocole est dit connecté et sécurisé, car il s’assure que toutes les données arrivent à destination et qu’elles soient reconstituées en prenant en compte leur ordre d’émission. Le mauvais côté de tout ceci est que TCP est beaucoup plus lent qu'UDP mais, comme vous le savez, UDP n'est pas un protocole sûr.

Alors pourquoi utiliser un protocole qui n'est pas sûr ?

Tout simplement car toutes les applications n'ont pas besoin de ce degré de sûreté. Un logiciel qui gère un FTP doit être sûr que toutes les données sont bien arrivées mais une application qui fait du streaming vidéo n'en a pas l'utilité, si une données est absente, vous ne le remarquerez même pas…
Certaines applications doivent privilégier la rapidité à la sécurité. Imaginez que vous regardiez un film en streaming et qu'une donnée se perde, le protocole TCP devra attendre que cette donnée soit envoyée de nouveau avant de poursuivre la lecture de votre film, conséquence, vous aurez une pause dans votre film. Pas terrible.

Il en va de même pour d'autres applications : les MMORPG (Massively Multiplayer Online Role-Playing Games, c’est-à-dire les jeux de rôle vidéos multi-joueurs en ligne). Les données des autres joueurs sont mises à jour plusieurs fois par secondes : si une donnée est manquante, ce n'est pas la fin du monde mais si l'application se bloque, là ça devient gênant. :-°

Dans ce chapitre nous allons donc voir comment utiliser ce protocole réseau côté client et côté serveur. Vous verrez qu'il y a quelques différences mais le raisonnement reste le même.

Présentation des protagonistes

TCP et UDP travaillant différemment, les objets que nous allons utiliser s'utiliseront donc différemment, mais je vous rassure, les différences restent minimes.
Déjà, nous n'utiliserons plus les classes vues précédemment et nous travaillerons avec les classes DatagramSocket et DatagramPacket.
La classe DatagramPacket représente les données qui seront envoyées via le protocole UDP : les paquets UDP s'appellent des datagrammes, d'où le nom de cette classe.
La classe DatagramSocket, elle, permet d'émettre ou de recevoir des datagrammes.

Il n'y a pas de classe ServerDatagramSocket alors ?

Et non. C'est une des différences avec le protocole TCP. Vu qu'il n'y a pas de mode connecté, il n'y a pas de raison de faire de vérification d'adresse de destination ni d'adresse d'expédition. UDP ne s'intéresse pas à qui envoie quoi. Il reçoit des données, point final.

Une autre différence de taille réside dans la façon de récupérer les données provenant d'un émetteur. Avec le protocole TCP, nous travaillions avec des flux (InputStream/OutputStream), comme lorsque nous travaillons avec des fichiers. Avec UDP, c'est différent. Vu qu'il n'y a aucun moyen de savoir si le premier datagramme reçu est bien le premier datagramme envoyé, le mode de lecture des données se résume au datagramme : donc pas de flux à gérer et la clé de voûte de nos futurs codes sera donc l'objet DatagramPacket.

La classe DatagramPacket

Cet objet va donc nous servir à envoyer des données sur le réseau à un destinataire mais avec une limite de taille. En fait, la plupart des plateformes mettent une limite à 8ko par datagramme UDP.
Cette classe se charge également de définir la source du datagramme et son destinataire, mais aussi de quel port il provient et vers quel port il est censé aller. C'est donc cet objet qui encapsule toutes les données réseaux pour la transmission.

Comment devons-nous faire pour savoir si le datagramme est à émettre ou à recevoir ?

C'est l'une des particularités de cet objet. Vous déterminez l'action à faire sur le datagramme en utilisant un constructeur différent selon les cas. Il y a un constructeur pour l'émission et un constructeur pour la réception de datagramme ,ce qui est assez inhabituel car normalement, les différents constructeurs d'un objets servent à définir des paramètres différents et non des comportements différents…

Voici les constructeurs que vous pouvez utiliser pour la réception de datagrammes :

public DatagramPacket(byte[] buffer, int length)
public DatagramPacket(byte[] buffer, int offset, int length)

Lorsqu'un serveur reçoit un datagramme, il stocke les données dans le buffer jusqu'à ce que tout le contenu du datagramme soit lu. Avec le premier constructeur, les données sont placées en commençant à l'emplacement buffer[0], alors qu'avec le second constructeur, les données sont placées en commençant à buffer[offset].

Voici les constructeurs que vous pouvez utiliser pour envoyer des données :

public DatagramPacket(byte[] data, int length, InetAddress destination, int port)
public DatagramPacket(byte[] data, int offset, int length, InetAddress destination, int port)
public DatagramPacket(byte[] data, int length, SocketAddress destination, int port)
public DatagramPacket(byte[] data, int offset, int length, SocketAddress destination, int port)

Vous retrouvez les mêmes éléments vus précédemment avec, en plus, l'adresse et le port de destination.
Voilà un exemple de création de datagramme à destination d'un serveur :

String test = "Coucou tout le monde !";
byte[] data = test.getBytes();
try {
   InetAddress adresse = InetAddress.getByName("127.0.0.1");
   int port = 2345;
   DatagramPacket dp = new DatagramPacket(data, data.length, adresse, port);
}catch (IOException ex){ }

C'est donc à nous de choisir la taille de nos datagrammes ? Comment faire alors ?

Là, tout dépend de votre application et de votre réseau. Si votre application transite sur un réseau local avec une bande passante à 100Mb/s, vous pouvez utiliser des datagrammes de 8Ko. Mais si vous êtes sur une liaison réseau fragile et instable, mieux vaut utiliser de petits datagrammes pour ne pas surcharger le réseau et ainsi limiter la quantité de données perdues - s'il y en a de perdues bien entendu, mais avec UDP, nous ne sommes au courant de rien…

L'objet DatagramPacket possède aussi des méthodes très utiles pour retrouver des informations sur la provenance d'un datagramme, en voici une liste non exhaustive :

  • getAddress( ) : retourne un objet InetAddress représentant l'adresse de l'émetteur ;

  • getSocketAddress( ) : retourne un objet SocketAddress représentant l'adresse de l'émetteur ainsi que son port d'émission ;

  • getPort( ) : retourne un entier représentant le port d'émission du datagramme ;

  • getData( ) : retourne un tableau de byte correspondant aux données du datagramme.

Vous pouvez voir que cette classe sera donc très sollicitée de part et d'autre de la communication réseau, mais celle-ci a tout de même besoin d'une connexion pour pouvoir fonctionner. Passons donc en revue la classe nous permettant de gérer ces connexions.

La classe DatagramSocket

Pour recevoir ou émettre un datagramme, vous aurez besoin de cet objet de chaque côté de la connexion. Tout comme pour l’objet vu précédemment, c'est avec le constructeur que nous allons différencier le côté client du côté serveur. Vous avez à disposition 5 constructeurs :

  • new DatagramSocket( ) : crée une connexion sans port de spécifié. Utile pour l'émetteur si celui-ci ne tient pas à trouver un port de disponible. Le système en affectera un automatiquement, mais ce n'est pas vous qui le choisissez.

  • new DatagramSocket(int port ) : ici, c'est vous qui choisissez le port de communication. Utilisé le plus souvent côté serveur où nous devons clairement définir un port de communication.

  • new DatagramSocket(int port, InetAddress adresse) : ce constructeur permet, en plus de spécifier un port, de spécifier une adresse d'écoute. Ceci est très utile si votre serveur a plusieurs adresses.

  • new DatagramSocket(SocketAddress adresse) : celui-ci fait exactement la même chose que le précédent. Et oui, dans un objet SocketAddress, vous avez l'adresse et le port. :) 

  • new DatagramSocket(DatagramSocketImpl impl) : permet à l'objet d'utiliser une implémentation de DatagramSocket personnalisée. Nous le l'utiliserons pas dans ce cours.

Et pour envoyer et recevoir des données ?

Il existe deux méthodes dans l'objet DatagramSocket qui permettent ceci :

  • La méthode send(DatagramPacket packet) : envoie le paquet ;

  • La méthode receive(DatagramPacket packet) : reçoit le paquet, mais bloque le thread courant jusqu'à réception d'un datagramme.

Voici comment les choses vont se passer : nous allons créer une socket UDP, nous allons créer des datagrammes contenant nos données à envoyer, et nous utiliserons notre socket pour envoyer nos datagrammes. C'est tout.

Communiquons en UDP

Maintenant que je vous ai présenté tout le monde, voici un code vous montrant comment créer une communication en utilisant UDP :

package udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

public class TestUDP {

   public final static int port = 2345;
   
   public static void main(String[] args){
    
      Thread t = new Thread(new Runnable(){
         public void run(){
            try {
               
               //Création de la connexion côté serveur, en spécifiant un port d'écoute
               DatagramSocket server = new DatagramSocket(port);
               
               while(true){
                  
                  //On s'occupe maintenant de l'objet paquet
                  byte[] buffer = new byte[8192];
                  DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                                   
                  //Cette méthode permet de récupérer le datagramme envoyé par le client
                  //Elle bloque le thread jusqu'à ce que celui-ci ait reçu quelque chose.
                  server.receive(packet);
                  
                  //nous récupérons le contenu de celui-ci et nous l'affichons
                  String str = new String(packet.getData());
                  print("Reçu de la part de " + packet.getAddress() 
                                    + " sur le port " + packet.getPort() + " : ");
                  println(str);
                  
                  //On réinitialise la taille du datagramme, pour les futures réceptions
                  packet.setLength(buffer.length);
                                    
                  //et nous allons répondre à notre client, donc même principe
                  byte[] buffer2 = new String("Réponse du serveur à " + str + "! ").getBytes();
                  DatagramPacket packet2 = new DatagramPacket(
                                       buffer2,             //Les données 
                                       buffer2.length,      //La taille des données
                                       packet.getAddress(), //L'adresse de l'émetteur
                                       packet.getPort()     //Le port de l'émetteur
                  );
                  
                  //Et on envoie vers l'émetteur du datagramme reçu précédemment
                  server.send(packet2);
                  packet2.setLength(buffer2.length);
               }
            } catch (SocketException e) {
               e.printStackTrace();
            } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            }
         }
      });  
      
      //Lancement du serveur
      t.start();
      
      Thread cli1 = new Thread(new UDPClient("Cysboy", 1000));
      Thread cli2 = new Thread(new UDPClient("John-John", 1000));
      
      cli1.start();
      cli2.start();
      
   }
   
   public static synchronized void print(String str){
      System.out.print(str);
   }
   public static synchronized void println(String str){
      System.err.println(str);
   }
   
   
  public static class UDPClient implements Runnable{
      String name = "";
      long sleepTime = 1000;
      
      public UDPClient(String pName, long sleep){
         name = pName;
         sleepTime = sleep;
      }
      
      public void run(){
         int nbre = 0;
         while(true){
            String envoi = name + "-" + (++nbre);
            byte[] buffer = envoi.getBytes();
            
            try {
               //On initialise la connexion côté client
               DatagramSocket client = new DatagramSocket();
               
               //On crée notre datagramme
               InetAddress adresse = InetAddress.getByName("127.0.0.1");
               DatagramPacket packet = new DatagramPacket(buffer, buffer.length, adresse, port);
               
               //On lui affecte les données à envoyer
               packet.setData(buffer);
               
               //On envoie au serveur
               client.send(packet);
               
               //Et on récupère la réponse du serveur
               byte[] buffer2 = new byte[8196];
               DatagramPacket packet2 = new DatagramPacket(buffer2, buffer2.length, adresse, port);
               client.receive(packet2);
               print(envoi + " a reçu une réponse du serveur : ");
               println(new String(packet2.getData()));
               
               try {
                  Thread.sleep(sleepTime);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
               
            } catch (SocketException e) {
               e.printStackTrace();
            } catch (UnknownHostException e) {
               e.printStackTrace();
            } catch (IOException e) {
               e.printStackTrace();
            }
         }
      }      
   }   
}

Ce qui me donne :

Client - serveur en UDP
Client - serveur en UDP

Ce code est très simple et ne présente aucune difficulté particulière. Je vous ai présenté les objets que nous allions utiliser dans la section précédente, ici nous avons juste mis en application ce que nous avons vu plus haut. Les commentaires du code sont assez complémentaires avec les explications précédentes, je me m'attarderai donc pas sur le sujet.

Finalement, le principe de fonctionnement du protocole UDP n’est pas beaucoup plus compliqué que celui du protocole TCP, non ? La seule chose qui change, c'est que nous spécifions l'hôte de destination au moment d'envoyer les données en lieu et place de l'initialisation de la connexion. Pour le reste, les concepts se rejoignent, et comme vous maîtrisez parfaitement le protocole TCP à ce stade, vous n’aurez pas dû être surpris !

Conclusion générale

Ce cours était assez costaud, il faut l'avouer. Vous avez appris à communiquer sur le réseau en TCP, en UDP et même à faire transiter des objets Java sur le réseau afin d'avoir des fonctionnements déportés.

J'espère que ce cours vous a plu et que vous avez appris beaucoup de choses utiles. :)

Comme je vous l’avais évoqué en introduction, la programmation réseau en Java est un sujet très vaste et ce cours ne couvre pas l’ensemble de ses fonctionnalités. Voici quelques pistes que vous pourriez creuser par vous-mêmes :

  • connexion sécurisée (SSL) ;

  • connexion non bloquante (SocketChannel) ;

  • ProtocolHandler ;

  • RMI ;

Si vous voulez creuser d’autres aspects du langage Java, n’hésitez pas à consulter mes cours suivants :

Example of certificate of achievement
Example of certificate of achievement