JAVA et réseau
Comme nous l'avons dit, un hôte sur le réseau peut être nommé soit par son adresse internet soit son nom DNS; leur manipulation se fait par la classe java.net.InetAddress.
public final class InetAddress extends Object implements Serializable {
public boolean isMulticastAddress()
public String getHostName()
public byte[] getAddress()
public String getHostAddress()
public int hashCode()
public boolean equals(Object obj)
public String toString()
public static InetAddress getByName(String host) throws UnknownHostException
public static InetAddress[] getAllByName(String host) throws UnknownHostException
public static InetAddress getLocalHost() throws UnknownHostException
}
|
Cette classe ne possède pas de constructeur. En général, les instances de InetAddress s'obtiennent comme résultat des méthodes static: getByName, getAllByName et getLocalHost.
public static InetAddress getByName (String host) throws UnknownHostException
Retourne un objet de la classe InetAddress codant l'adresse IP d'un hôte. L'argument host est le nom DNS de l'hôte ou son adresse IP.
public static InetAddress[] getAllByName (String host) throws UnknownHostException
Retourne un tableau d'objets de la classe InetAddress codant toutes les adresses IP d'un hôte. L'argument host est le nom DNS de l'hôte ou son adresse IP.
public static InetAddress getLocalHost () throws UnknownHostException
Retourne un objet de la classe InetAddress codant l'adresse IP du hôte local.
public String getHostName ()
Retourne une chaîne de caractères contenant le nom d'hôte de l'objet InetAddress ou son adresse IP (si le nom n'existe pas).
public byte[] getAddress ()
Retourne un tableau d'octet contenant les 4 octets de l'adresse IP de l'objet InetAddress.
public String getHostAddress ()
Retourne une chaîne de caractères contenant les 4 octets de l'adresse IP de l'objet InetAddress; cette chaîne est de la forme "%d.%d.%d.%d".
import java.net.*;
public class Adresses {
public static void main(String[] args) {
for(int i=0 ; i< args.length; i++) {
try {
InetAddress[] inetadrs = InetAddress.getAllByName(args[i]);
System.out.println(args[i] + ":");
for(int j=0 ; j<inetadrs.length ; j++) System.out.println(inetadrs[j]);
}
catch (UnknownHostException e) { System.out.println(args[i] + " inconnu !!! "); }
}
}
}
|
TCP/IP offre, en particulier, une interface dite sockets pour l'écriture d'applications communicantes. Les sockets a été introduit avec le système d'exploitation UNIX de Berkeley. L'idée des sockets est de faire en sorte que deux programmes sur des hôtes différents puissent communiquer l'un avec l'autre à travers sans se soucier des détails de bas niveau de la communication réseau.
Les primitives TCP pour les sockets permettent :
l'attente passive de connexions TCP initiées par des applications distantes ;
l'initiation de connexions "actives" vers des applications distantes en attente passive ;
l'acceptation ou le refus d'une connexion initiée par une application distante ;
l'envoi et la réception de données sur une connexion établie ;
la terminaison brutale ou ordonnée d'une connexion établie ;
le test d'une connexion.
Les sockets ne sont pas des objets spécifiquement réseaux, et encore moins spécifiquement TCP/IP. Ce sont des objets génériques qui doivent être paramétrés lors de leur création selon l'utilisation prévue :
socket pour communication inter-processus classique
socket pour communication réseau en mode datagramme
socket pour communication réseau en mode connecté
Dans la communication par sockets, il existe toujours une machine qui joue le rôle du serveur et l'autre (ou les autres) qui joue le rôle de client.
Le serveur est une application qui tourne sur un hôte et qui est à l'écoute des requêtes d'un ou de plusieurs clients sur un port particulier.
Le client est une application qui tourne un hôte (pas forcément différent de celui sur lequel tourne le serveur). Il doit connaître l'hôte et le port sur lequel le serveur est à l'écoute; le client tente alors une connexion au serveur sur l'hôte et le port approprié.
Lorsque le serveur est conçu pour communiquer avec plusieurs clients, quand tout se passe bien,
il connecte le client sur un nouveau numéro de port
il se remet en attente de connexion sur le port original
Le client et le serveur peuvent alors communiquer l'un avec l'autre en écrivant et en lisant sur les sockets.
Les programmes serveurs doivent toujours être actifs lorsque des clients initient une connexion. Il existe deux façons d'assurer que ces serveurs soient activés :
lancement "manuel" des programmes serveurs,
utilisation du "super serveur INETD".
Le lancement manuel signifie que le programme serveur s'exécute en permanence, même s'il n'est pas appelé par des programmes clients. Compte tenu du grand nombre de services réseaux disponibles sur une machine donnée, de nombreux processus, oisifs en attente mais consommant du temps CPU et des ressources, existent sur le système. Cette solution est peu recommandée.
La deuxième solution est l'utilisation d'un serveur particulier, le démon inetd, qui réalise les attentes pour le compte d'autres programmes serveurs. De plus, l'utilisation du réseau par un démon lancé par inetd est cachée : le programmeur utilise les descripteurs de fichiers correspondant à l'entrée et à la sortie standard du programme. La redirection de ces deux descripteurs vers un socket est réalisée de façon implicite et systématique par le processus inetd lui-même.
Le démon inetd, lors de son activation, consulte un fichier de configuration décrivant les services à assurer sur le système. Le fichier s'appelle /etc/inetd.conf, et contient un ensemble de lignes au format suivant :
service stream tcp nowait root /usr/etc/progd progd |
Une telle ligne signifie que service est implanté sur cette machine, que les sockets utilisés sont de type stream, que le protocole utilisé est tcp, que le programme à activer lors d'une demande de service est /usr/etc/progd, que le nom à donner au programme sera progd, que le programme doit s'exécuter avec comme propriétaire root.
Le paramètre nowait indique que plusieurs instances de ce programme peuvent être activées sans attendre la terminaison des instances précédantes.
Les ports à utiliser pour les sockets (ici les sockets TCP) sont définis dans un fichier particulier, /etc/services, qui contiendra la ligne suivante :
service 78/tcp alias_service |
Cette ligne indique que pour le service défini dans le fichier inetd.conf, il faut utiliser le port 78 du protocole tcp. Le reste de la ligne est un alias possible pour le nom du service. Une fois ces informations récupérées, le serveur inetd va créer un socket, lui associer le numéro de port, et se mettre en attente d'une indication d'établissement de connexion à ce port. Comme de nombreux services existent, le superserveur inetd crée de nombreux sockets, assigne les numéros de port adéquats, se met en attente, etc..
Dans le cas de service utilisant udp, le fichier inetd.conf contient des informations légèrement différentes :
service dgram udp wait root /usr/etc/progd progd |
Le paramètre dgram identifie un socket de type datagramme, udp indique que le protocole utilisé est UDP, wait indique qu'il faut attendre la terminaison d'une instance du service avant de pouvoir en lancer la suivante. Notons que si les programmes serveurs sont conçus pour s'exécuter en parallèle (voir chapitre 5), on peut trouver la ligne suivante dans le fichier /etc/inetd.conf :
service dgram udp nowait root /usr/etc/progd progd |
Dans les deux cas, on devra trouver la ligne correspondante dans le fichier /etc/services :
service 78/udp alias_service |
Java fournit deux classes pour la gestion des sockets:
la classe Socket qui permet d'initialiser la connexion avec un serveur
la classe ServerSocket qui permet de créer un serveur à l'écoute des connexions à venir.
public class Socket {
protected Socket()
protected Socket(SocketImpl impl) throws SocketException
public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(String host, int port, boolean stream) throws IOException
public Socket(InetAddress host, int port, boolean stream) throws IOException
public InetAddress getInetAddress()
public InetAddress getLocalAddress()
public int getPort()
public int getLocalPort()
public InputStream getInputStream() throws IOException
public OutputStream getOutputStream() throws IOException
public void setTcpNoDelay(boolean on) throws SocketException
public boolean getTcpNoDelay() throws SocketException
public void setSoLinger(boolean on, int val) throws SocketException
public int getSoLinger() throws SocketException
public void setSoTimeout(int timeout) throws SocketException
public int getSoTimeout() throws SocketException
public void close() throws IOException
public String toString()
public static void setSocketImplFactory(SocketImplFactory fac) throws IOException
}
|
Une application cliente doit ouvrir une connexion sur en créant un socket.
try {
socket sock = new Socket("www.univ-nc.nc", 2000);
}
catch (UnknoxnHostException e) { System.out.println("Hôte inconnu"); System.exit(-1); }
catch (IOException e) { System.out.println("Erreur lors de connexion"); System.exit(-1);}
|
La connexion établie, les canaux d'entrée/sortie s'obtiennent grâce aux méthode getInputStream et getOutputStream.
Pour tester le client, mous allons utiliser une application serveur qui existe sur tous les systèmes UNIX et qui est à l'écoute du port 7: il s'agit du serveur echo qui se contente de renvoyer au client la chaîne que ce dernier lui envoie.
import java.io.*;
import java.net.*;
public class Client {
public static void main(String[] args) throws Exception, NumberFormatException {
Socket serveur = new Socket("www.univ-nc.nc", Integer.parseInt(args[0]));
PrintWriter Sout = new PrintWriter(serveur.getOutputStream());
BufferedReader Sin = new BufferedReader(new InputStreamReader(serveur.getInputStream()));
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String jEnvoie, jeReçois;
do {
jEnvoie = in.readLine();
Sout.println(jEnvoie);
Sout.flush();
jeReçois = Sin.readLine();
System.out.println(jeReçois);
}
while (! jEnvoie.equals("fin"));
Sin.close();
Sout.close();
serveur.close();
}
}
|
public class ServerSocket {
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
public InetAddress getInetAddress()
public int getLocalPort()
public Socket accept() throws IOException
protected final void implAccept(Socket s) throws IOException
public void close() throws IOException
public void setSoTimeout(int timeout) throws SocketException
public int getSoTimeout() throws IOException
public String toString()
public static void setSocketFactory(SocketImplFactory fac) throws IOException
}
|
Une application serveur doit créer un instance ServerSocket:
try {
ServerSocket sock = new ServerSocket(2000);
}
catch (IOException e) {
System.out.println("Erreur de création du serveur sur le port 2000");
System.exit(-1);
}
|
Une fois le serveur crée, il va se mettre en attente d'une connexion cliente:
Socket sockClient;
try {
sockClient= serverSocket.accept();
}
catch (IOException e) {
System.out.println("Echec de la mise en attente sur le port 2000");
System.exit(-1);
}
|
La méthode accept permet au serveur d'être à l'écoute du port 2000 et d'attendre une future connexion cliente. Lorsqu'une telle connexion s'établit, la varibale sockClient permet de récupérer le canal de communication établit avec le client grâce aux méthodes getInputStream et getOutputStream.
Pour illustrer le fonctionnement du serveur, programmons le fameux serveur echo.
import java.io.*;
import java.net.*;
public class TestEcho {
public static void main(String[] args) throws Exception {
ServerSocket serveur = new ServerSocket(2000);
while (true) {
Socket client = serveur.accept();
PrintWriter Cout = new PrintWriter(client.getOutputStream());
BufferedReader Cin = new BufferedReader(new InputStreamReader(client.getInputStream()));
String jeReçois;
do {
jeReçois = Cin.readLine();
Cout.println("Vous avez dit " + jeReçois);
Cout.flush();
}
while (! jeReçois.equals("fin"));
Cin.close();
Cout.close();
client.close();
}
}
}
|
Le serveur TestEcho est bien trop simpliciste. En effet, ce serveur n'accepte qu'un client à la fois. Que se passe-t-il si plusieurs clients essayent de se connecter en même temps ? Le premier qui se connecte monopolise la connexion et les autres clients doivent attendre la fin de communication du premier pour pouvoir dialoguer avec le serveur à tour de rôle.
Cette solution est très restrictive. Nous allons modifier le serveur pour qu'il puisse répondre à plusieurs clients à la fois. Pour ce faire, le serveur devra créer un thread par client connecté de manière à pouvoir se remettre en attente d'un éventuel futur client.
Le squelette d'un programme qui réalise ceci est de la forme:
while (true) {
attente d'un connexion
créer un thread pour dialoguer avec le client qui s'est connecté
}
|
import java.io.*;
import java.net.*;
public class TestEchoMultiple {
public static void main(String[] args) throws Exception {
ServerSocket serveur = new ServerSocket(2000);
while (true) {
Socket client = serveur.accept(); System.out.println("C'est par");
new EncoreUnClient(client).start();
}
}
}
class EncoreUnClient extends Thread {
Socket client;
public EncoreUnClient(Socket client) {
super("Clients");
this.client = client;
}
public void run() {
PrintWriter Cout = null;
BufferedReader Cin = null;
try {
System.out.println("C'est parti");
Cout = new PrintWriter(client.getOutputStream());
Cin = new BufferedReader(new InputStreamReader(client.getInputStream()));
String jeReçois;
do {
jeReçois = Cin.readLine();
Cout.println("Vous avez dit " + jeReçois);
Cout.flush();
}
while (! jeReçois.equals("fin"));
if (Cin != null) Cin.close();
if (Cout != null) Cout.close();
client.close();
}
catch (IOException e) { e.printStackTrace(); }
}
}
|
On dispose de deux classes pour la communication par UDP:
la class DatagramPacket qui en charge de l'emballage et du déballage des données en paquets.
la class DatagramSocket qui se charge d'envoyer et de recevoir les paquets frabriqués par un DatagramPacket.
Ainsi, l'envoi de données par UDP consite à les confier à un DatagramPacket en vue de leur emballage sous forme de paquets et à envoyer ce DatagramPacket à un DatagramSocket pour leur expédition. Un DatagramSocket se charge de la récéption; les paquets reçus doivent être mis dans un DatagramPacket en vue de leur déballage.
public final class DatagramPacket {
public DatagramPacket(byte ibuf[], int ilength)
public DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport)
public synchronized InetAddress getAddress()
public synchronized int getPort()
public synchronized byte[] getData()
public synchronized int getLength()
public synchronized void setAddress(InetAddress iaddr)
public synchronized void setPort(int iport)
public synchronized void setData(byte ibuf[])
public synchronized void setLength(int ilength)
}
|
La classe DatagramPacket dispose de deux constructeurs
public DatagramPacket(byte ibuf[], int ilength) public DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport) |
où
ibuf désigne le tampon,
ilength désigne la taille du paquet à envoyer (forcément inférieure à la taille du tampon),
iaddr l'adresse de destination,
iport le port de destination.
Le premier constructeur est utiliser pour recevoir les paquets et le deuxième pour en envoyer.
Les méthodes getAddress, getPort, getLength et getData permettent de récupérer respectivement l'adresse de destination, le port de destination, la taille du paquet et les données contenues dans le paquet.
Les méthodes setAddress, setPort, setLength et setData assigne respectivement l'adresse de destination, le port de destination, la taille du paquet et les données contenues dans le paquet.
Class java.net.DatagramSocket {
public DatagramSocket() throws SocketException
public DatagramSocket(int port) throws SocketException
public DatagramSocket(int port, InetAddress laddr) throws SocketException
public void send(DatagramPacket p) throws IOException
public synchronized void receive(DatagramPacket p) throws IOException
public InetAddress getLocalAddress()
public int getLocalPort()
public synchronized void setSoTimeout(int timeout) throws SocketException
public synchronized int getSoTimeout() throws SocketException
public void close()
}
|
La classe DatagramSocket possède trois constructeurs:
public DatagramSocket() throws SocketException |
Crée un socket datagramme et l'associe à un port quelconque de la machine locale.
public DatagramSocket(int numport) throws SocketException |
Crée un socket datagramme et l'associe à un port numport de la machine locale.
public DatagramSocket(int numport, InetAddress laddr) throws SocketException |
Crée un socket datagramme et l'associe à un port numport de la machine d'adresse laddr.
Les méthodes send et receive de cette classe permettent d'envoyer et de recevoir les packets UDP.
La méthode send lève une exception lorsque le SecurityManager refuse l'envoi d'un paquet vers l'hôte spécifié. C'est le cas ou une erreur peut se produire.
La méthode receive (tout comme la méthode accept de la classe SocketServer) se met en attente et lève également une exception lorsqu'il y une erreur dans la reception du paquet UDP. La méthode receive doit disposer d'un tampon assez grand pour le packet à recevoir. Dans le cas contraire, le paquet est tronqué à la taille du tampon.
Une application cliente doit ouvrir une connexion sur en créant un socket.
try {
socket sock = new Socket("www.univ-nc.nc", 2000);
}
catch (UnknoxnHostException e) { System.out.println("Hôte inconnu"); System.exit(-1); }
catch (IOException e) { System.out.println("Erreur lors de connexion"); System.exit(-1);}
|
La connexion établie, les canaux d'entrée/sortie s'obtiennent grâce aux méthode getInputStream et getOutputStream.
Pour tester le client, mous allons utiliser une application serveur qui existe sur tous les systèmes UNIX et qui est à l'écoute du port 7: il s'agit du serveur echo qui se contente de renvoyer au client la chaîne que ce dernier lui envoie.
import java.io.*;
import java.net.*;
public class Client {
public static void main(String[] args) throws Exception, NumberFormatException {
Socket serveur = new Socket("www.univ-nc.nc", Integer.parseInt(args[0]));
PrintWriter Sout = new PrintWriter(serveur.getOutputStream());
BufferedReader Sin = new BufferedReader(new InputStreamReader(serveur.getInputStream()));
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String jEnvoie, jeReçois;
do {
jEnvoie = in.readLine();
Sout.println(jEnvoie);
Sout.flush();
jeReçois = Sin.readLine();
System.out.println(jeReçois);
}
while (! jEnvoie.equals("fin"));
Sin.close();
Sout.close();
serveur.close();
}
}
|
Et voici à présent un serveur UDP à l'image du serveur TCP que nous vu. Il faut donc :
Créer un DatagramSocket
DatagramSocket socket = new DatagramSocket(2001); |
Créer un paquet pour recevoir les données
DatagramPacket paquet = new DatagramPacket(buf, buf.length); |
Se mettre en attente d'un paquet
socket.receive(paquet); |
Récupérer l'expéditeur (hôte et port) et le contenu du message
InetAddress address = paquet.getAddress(); int port = paquet.getPort(); jeReçois = new String(paquet.getData(), 0, paquet.getLength()); |
Renvoyer un message à l'expéditeur dans un paquet
paquet = new DatagramPacket(buf, buf.length, address, port); socket.send(paquet); |
Et voici le programme complet.
import java.io.*;
import java.net.*;
public class UdpTestEcho {
public static void main(String[] args) throws Exception {
String jeReçois;
DatagramSocket socket = new DatagramSocket(2001);
while (true) {
byte[] buf = new byte[56];
DatagramPacket paquet = new DatagramPacket(buf, buf.length);
socket.receive(paquet);
jeReçois = new String(paquet.getData(), 0, paquet.getLength());
System.out.println("j'ai recu : " + jeReçois + " " + jeReçois.length());
InetAddress address = paquet.getAddress();
int port = paquet.getPort();
paquet = new DatagramPacket(buf, buf.length, address, port);
socket.send(paquet);
}
}
}
|