La communication par socket en Java
Ce document est aussi disponible en postscript.
L'interface socket BSD est l'interface de programmation réseau la plus
courante. Cette interface permet de façon classique d'utiliser une
communication par socket selon le schéma habituel
client-serveur. L'interface Java des sockets (package java.net)
offre un accès simple aux sockets sur IP.

1 Les classes
Plusieurs classes interviennent lors de la réalisation d'une communication
par sockets. La classe java.net.InetAddress permet de manipuler
des adresses IP. La classe java.net.SocketServer permet de
programmer l'interface côté serveur en mode connecté. La classe
java.net.Socket permet de programmer l'interface côté client et la
communication effective par flot via les sockets. Les classes
java.net.DatagramSocket et java.net.DatagramPacket
permettent de programmer la communication en mode datagramme.
1.1 La classe java.net.InetAddress
Cette classe représente les adresses IP et un ensemble de méthodes pour les
manipuler. Elle encapsule aussi l'accès au serveur de noms DNS.
public class InetAddress implements Serializable
1.1.1 Les opérations de la classe InetAddress
Conversion de nom vers adresse IP :
Un premier ensemble de méthodes permet de créer des objets adresses IP.
public static InetAddress getLocalHost() throws UnknownHostException
Cette méthode renvoie l'adresse IP du site local d'appel.
public static InetAddress getByName(String host) throws UnknownHostException
Cette méthode construit un nouvel objet InetAddress à partir d'un
nom textuel de site. Le nom du site est donné sous forme symbolique
(bach.enseeiht.fr) ou sous forme numérique (147.127.18.03).
Enfin,
public static InetAddress[] getAllByName(String host) throws UnknownHostException
permet d'obtenir les différentes adresses IP d'un site.
Conversions inverses
Des méthodes applicables à un objet de la classe InetAddress
permettent d'obtenir dans divers formats des adresses IP ou des noms de
site. Les principales sont :
| public String getHostName() |
obtient le nom complet correspondant à l'adresse IP |
| public String getHostAddress() |
obtient l'adresse IP sous forme %d.%d.%d.%d |
| public byte[] getAddress() |
obtient l'adresse IP sous forme
d'un tableau d'octets. |
1.2 La classe ServerSocket
Cette classe implante un objet ayant un comportement de serveur
via une interface par socket.
public class java.net.ServerSocket
Une implantation standard du service existe mais peut être redéfinie en
donnant une implantation explicite sous la forme d'un objet de la classe
java.net.SocketImpl. Nous nous contenterons d'utiliser la version
standard.
1.2.1 Le constructeur 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
Ces constructeurs créent un objet serveur à l'écoute du port spécifié. La
taille de la file d'attente des demandes de connexion peut être
explicitement spécifiée via le paramètre backlog. Si la machine
possède plusieurs adresses, on peut aussi restreindre l'adresse sur laquelle
on accepte les connexions.
Appels système :
ce constructeur correspond à l'utilisation des
primitives socket, bind et listen.
1.2.2 Les opérations de la classe ServerSocket
Nous ne retiendrons que les méthodes de base. La méthode essentielle est
l'acceptation d'une connexion d'un client :
public Socket accept() throws IOException
Cette méthode est bloquante, mais l'attente peut être limitée dans le temps
par l'appel préalable de la méthode setSoTimeout. Cette méthode
prend en paramètre le délai de garde exprimé en millisecondes. La valeur par
défaut 0 équivaut à l'infini. À l'expiration du délai de garde,
l'exception java.io.InterruptedIOException est levée.
public void setSoTimeout(int timeout) throws SocketException
La fermeture du socket d'écoute s'exécute par l'appel de la méthode
close.
Enfin, les méthodes suivantes retrouvent l'adresse IP ou le port d'un socket
d'écoute :
public InetAddress getInetAddress()
public int getLocalPort()
1.3 La classe java.net.Socket
La classe java.net.Socket est utilisée pour la programmation des
sockets connectés, côté client et côté serveur.
public class java.net.Socket
Comme pour le serveur, nous utiliserons l'implantation standard
bien qu'elle soit redéfinissable par le développement d'une nouvelle
implantation de la classe java.net.SocketImpl.
1.3.1 Constructeurs
Côté serveur, la méthode accept de la classe
java.net.ServerSocket renvoie un socket de service connecté au
client. Côté client, on utilise :
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 UnknownHostException, IOException
public Socket(InetAddress addr, int port, InetAddress localAddr, int localPort)
throws IOException
Les deux premiers constructeurs construisent un socket connecté à la machine
et au port spécifiés. Par défaut, la connexion est de type TCP fiable. Les
deux autres interfaces permettent en outre de fixer l'adresse IP et le
numéro de port utilisés côté client (plutôt que d'utiliser un port
disponible quelconque).
Appels système :
ces constructeurs correspondent à l'utilisation
des primitives socket, bind (éventuellement) et
connect.
1.3.2 Opérations de la classe Socket
La communication effective sur une connexion par socket utilise la notion de
flots de données (java.io.OutputStream et
java.io.InputStream). Les deux méthodes suivantes sont utilisés
pour obtenir les flots en entrée et en sortie.
public InputStream getInputStream() throws IOException
public OutputStream getOutputStream() throws IOException
Les flots obtenus servent de base à la construction d'objets de classes plus
abstraites telles que java.io.DataOutputStream et
java.io.DataInputStream (pour le JDK1), ou
java.io.PrintWriter et java.io.BufferedReader (JDK2) (cf
exemple 2).
Une opération de lecture sur ces flots est bloquante tant que des données ne
sont pas disponibles. Cependant, il est possible de fixer une délai de garde
à l'attente de données (similaire au délai de garde du socket d'écoute :
levée de l'exception java.io.InterruptedIOException) :
public void setSoTimeout(int timeout) throws SocketException
Un ensemble de méthodes permet d'obtenir les éléments constitutifs de la
liaison établie :
| public InetAddress getInetAddress() |
fournit l'adresse IP distante |
| public InetAddress getLocalAddress() |
fournit l'adresse IP locale |
| public int getPort() |
fournit le port distant |
| public int getLocalPort() |
fournit le port local |
L'opération close ferme la connexion et libère les ressources
du système associées au socket.
1.4 Socket en mode datagramme DatagramSocket
La classe java.net.DatagramSocket permet d'envoyer et de recevoir
des paquets (datagrammes UDP). Il s'agit donc de messages non fiables
(possibilités de pertes et de duplication), non ordonnés (les messages
peuvent être reçus dans un ordre différent de celui d'émission) et dont la
taille (assez faible -- souvent 4Ko) dépend du réseau sous-jacent.
public class java.net.DatagramSocket
1.4.1 Constructeurs
public DatagramSocket() throws SocketException
public DatagramSocket(int port) throws SocketException
Construit un socket datagramme en spécifiant éventuellement un port sur la
machine locale (par défaut, un port disponible quelconque est choisi).
1.4.2 Émission/réception
public void send(DatagramPacket p) throws IOException
public void receive(DatagramPacket p) throws IOException
Ces opérations permettent d'envoyer et de recevoir un paquet. Un paquet est
un objet de la classe java.net.DatagramPacket qui possède une zone
de données et (éventuellement) une adresse IP et un numéro de port
(destinataire dans le cas send, émetteur dans le cas
receive). Les principales méthodes sont :
public DatagramPacket(byte[] buf, int length)
public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
public InetAddress getAddress()
public int getPort()
public byte[] getData()
public int getLength()
public void setAddress(InetAddress iaddr)
public void setPort(int iport)
public void setData(byte[] buf)
public void setLength(int length)
Les constructeurs renvoient un objet pour recevoir ou émettre des
paquets. Les accesseurs get* permettent, dans le cas d'un
receive, d'obtenir l'émetteur et le contenu du message. Les
méthodes de modification set* permettent de changer les paramètres
ou le contenu d'un message pour l'émission.
Il est possible de « connecter » un socket datagramme à un
destinataire. Dans ce cas, les paquets émis sur le socket seront toujours
pour l'adresse spécifiée. La connexion simplifie l'envoi d'une série de
paquets (il n'est plus nécessaire de spécifier l'adresse de destination pour
chacun d'entre eux) et accélère les contrôles de sécurité (ils ont lieu une
fois pour toute à la connexion). La « déconnexion » enlève l'association (le
socket redevient disponible comme dans l'état initial).
public void connect(InetAddress address, int port)
public void disconnect()
Diverses méthodes renvoient le numéro de port local et l'adresse de la
machine locale (getLocalPort et getLocalAddress), et dans
le cas d'un socket connecté, le numéro de port distant et l'adresse distante
(getPort et getInetAddress). Comme précédemment, on peut
spécifier un délai de garde pour l'opération receive avec
setSoTimeout. On peut aussi obtenir ou réduire la taille maximale
d'un paquet avec getSendBufferSize, getReceiveBufferSize,
setSendBufferSize et setReceiveBufferSize.
Enfin, n'oublions pas la méthode close qui libère les ressources
du système associées au socket.
2 Exemple
Dans cet exemple, une communication entre un processus client et
un processus serveur est établie. Les deux processus échangent
des messages sous forme de lignes de texte.
Le processus client commence par émettre un message et le serveur
lui répond par un écho de cette ligne. Au bout de 10 échanges, le
client envoie une message de terminaison et ferme la connexion.
2.1 Le programme du serveur
import java.io.*;
import java.net.*;
public class Serveur {
static final int port = 8080;
public static void main(String[] args) throws Exception {
ServerSocket s = new ServerSocket(port);
Socket soc = s.accept();
// Un BufferedReader permet de lire par ligne.
BufferedReader plec = new BufferedReader(
new InputStreamReader(soc.getInputStream())
);
// Un PrintWriter possède toutes les opérations print classiques.
// En mode auto-flush, le tampon est vidé (flush) à l'appel de println.
PrintWriter pred = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(soc.getOutputStream())),
true);
while (true) {
String str = plec.readLine(); // lecture du message
if (str.equals("END")) break;
System.out.println("ECHO = " + str); // trace locale
pred.println(str); // renvoi d'un écho
}
plec.close();
pred.close();
soc.close();
}
}
2.2 Le programme du client
import java.io.*;
import java.net.*;
/** Le processus client se connecte au site fourni dans la commande
* d'appel en premier argument et utilise le port distant 8080.
*/
public class Client {
static final int port = 8080;
public static void main(String[] args) throws Exception {
Socket socket = new Socket(args[0], port);
System.out.println("SOCKET = " + socket);
BufferedReader plec = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
PrintWriter pred = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())),
true);
String str = "bonjour";
for (int i = 0; i < 10; i++) {
pred.println(str); // envoi d'un message
str = plec.readLine(); // lecture de l'écho
}
System.out.println("END"); // message de terminaison
pred.println("END") ;
plec.close();
pred.close();
socket.close();
}
}
3 Quizz
-
Dans l'exemple précédent, le client et le serveur se synchronisent
pour fermer la connexion. Modifier le comportement du client pour qu'il
s'arrête avant le serveur. Que se passe-t-il côté serveur ? Faites le test
inverse : arrêt prématuré du serveur.
- Dans l'exemple précédent, le client et le serveur communiquent
alternativement. Programmer une version où le client et le serveur
communiquent simultanément. Chacun envoie à l'autre un message, puis chacun
lit le message de l'autre.
- Développer le serveur de façon à ce qu'il puisse gérer plusieurs
clients simultanément : un thread sera dédié à chaque client et gérera la
connexion avec celui-ci.
Ce document a été traduit de LATEX par
HEVEA.