11 Serveurs réseau En pratique, il y a bien plus de chances que vous écriviez du code de client réseau que du code de serveur réseau. Toutefois, bon nombre d applications intègrent à la fois des fonctionnalités client et des fonctionnalités serveur. Java propose un excellent support pour les deux. Le paquetage java.net fournit les fonctionnalités de communication réseau côté serveur que nous utiliserons dans ce chapitre. La plate-forme J2EE (que nous n abordons pas dans ce livre) propose de nombreux services réseau supplémentaires dont un support complet du développement web Java côté serveur. Parmi les technologies réseau incluses dans la plate-forme J2EE, on peut citer les servlets, les EJB et le JMS.
132 CHAPITRE 11 Serveurs réseau Créer un serveur et accepter une requête public static final short PORT = 9988; ServerSocket server = new ServerSocket(PORT); while ((clientsock = server.accept( ))!= null) { // Traiter la demande du client Cet exemple utilise une instance de ServerSocket pour créer un serveur écoutant sur le port 9988. Nous passons le port sur lequel nous souhaitons que le serveur écoute au constructeur du ServerSocket. Une fois le socket serveur créé, nous appelons la méthode accept() pour attendre une connexion client. La méthode accept() bloque l exécution jusqu à ce qu une connexion avec un client soit opérée. Lorsqu une connexion est établie, une nouvelle instance Socket est retournée. Si un gestionnaire de sécurité est utilisé, sa méthode check- Accept() est appelée avec clientsock.getinetaddress(). gethostaddress() et clientsock.getport() en arguments, afin de s assurer que l opération est autorisée. Cette vérification peut lever une exception SecurityException. Les exemples de ce chapitre utilisent tous la classe ServerSocket. Elle est utilisée par le serveur pour attendre et établir les connexions client. L exemple précédent l a montré : lorsque vous commencez par créer un objet de la classe ServerSocket, vous devez spécifier un port à écouter pour les requêtes entrantes. La classe ServerSocket elle-même n est pas utilisée pour la
Retourner une réponse 133 communication avec le client, mais uniquement pour établir une connexion avec lui. Lorsque ServerSocket accepte une connexion client, une instance Socket standard est retournée. C est cette instance qui est utilisée pour communiquer avec le client. Pour plus d informations sur la manière d écrire votre code lorsque vous vous attendrez à devoir gérer de nombreuses requêtes clientes simultanées, consultez l exemple Gérer plusieurs clients de ce chapitre. Retourner une réponse Socket clientsock = serversocket.accept(); DataOutputStream out = new DataOutputStream(clientSock.getOutputStream()); out.writeint(somevalue); out.close(); Cet exemple montre comment retourner une réponse du serveur au client. La méthode accept() de l instance ServerSocket retourne une instance Socket lorsqu une connexion est établie avec un client. Nous obtenons ensuite le flux de sortie de ce socket en appelant la méthode getoutputstream() de cet objet. Nous utilisons le flux de sortie pour instancier un DataOutputStream et appelons la méthode writeint() de ce dernier pour écrire une valeur entière envoyée sous forme de données binaires au client. Pour finir, nous fermons le socket en utilisant la méthode close() du Socket. Cet exemple utilise la méthode write(), mais DataOut - putstream possède bien d autres méthodes pour écrire des données depuis n importe quel type de données Java
134 CHAPITRE 11 Serveurs réseau primitif, et notamment les suivantes : write(), writeboolean(), writebyte(), writebytes(), writechar(), writechars(), writedouble(), writefloat(), writeint(), writelong() et writeshort(). Pour plus d informations sur l utilisation de ces méthodes et des autres méthodes de la classe DataOutputStream, consultez la JavaDoc à l adresse http:// download.oracle.com/javase/7/docs/api/java/io/ DataOutputStream.html. Si vous souhaitez écrire des données texte au client, utilisez le code suivant : Socket clientsock = serversocket.accept(); PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSock.getOutputStream()), true); out.println( Hello World ); out.close(); Au lieu de créer un DataOutputStream, nous créons cette fois un OutputStreamWriter et un PrintWriter. La méthode print() du PrintWriter permet d écrire une chaîne de texte à destination du client. Le second paramètre passé au constructeur PrintWriter définit l option de purge automatique. La valeur true amène les méthodes println(), printf() et format() à vider automatiquement le tampon de sortie. Comme notre exemple utilise la méthode println(), il n est pas nécessaire d appeler explicitement la méthode flush(). Enfin comme toujours, lorsque nous avons fini d utiliser le PrintWriter, nous appelons la méthode close() pour fermer le flux. Retourner un objet Socket clientsock = serversocket.accept(); ObjectOutputStream os = new ObjectOutputStream(
Retourner un objet 135 clientsock.getoutputstream( )); // Retourner un objet os.writeobject(new Date()); os.close(); Cet exemple retourne un objet sérialisé au client. Nous obtenons une instance Socket que retourne la méthode accept() du ServerSocket une fois qu une connexion à un client est établie. Nous créons alors une instance ObjectOutputStream et nous passons le flux de sortie obtenu depuis le socket client. ObjectOutputStream est utilisée pour écrire des types de données primitifs et des graphes d objets Java vers un OutputStream. Dans cet exemple, nous écrivons un objet Date dans le flux de sortie puis nous fermons ce flux. La méthode writeobject() sérialise l objet passé en paramètre. Dans cet exemple, il s agit d un objet Date. Tous les champs de données qui ne sont pas transitoires et dynamiques sont préservés dans la sérialisation et restaurés lorsque l objet est désérialisé. Seuls les objets qui supportent l interface java.io.serializable peuvent être sérialisés. La librairie XStream de codehaus.org propose une alternative intéressante aux classes ObjectOutputStream et ObjectInputStream. XStream fournit des implémentations de ObjectInputStream et de ObjectOutputStream qui permettent aux flux d objets d être sérialisés ou désérialisés en XML. La classe ObjectInputStream standard utilise un format binaire pour les données sérialisées. La sortie sérialisée des classes XStream fournit pour sa part les classes sérialisées dans un format XML facile à lire. Pour plus d informations sur XStream et pour télécharger ce projet, rendez-vous à l adresse http://xstream.codehaus.org/.
136 CHAPITRE 11 Serveurs réseau Gérer plusieurs clients while (true) { Socket clientsock = socket.accept(); new Handler(clientSock).start(); Pour gérer plusieurs clients, il suffit de créer un thread pour chaque requête entrante à traiter. Dans cet exemple, nous créons un nouveau thread pour gérer la connexion cliente entrante immédiatement après avoir accepté la connexion. Ce procédé permet de libérer notre thread d écouteur de serveur qui peut retourner écouter les connexions clientes suivantes. Ici, nous nous trouvons à l intérieur d une boucle while infinie : dès qu un thread est engendré pour gérer une requête entrante, le serveur retourne immédiatement attendre la requête suivante. La classe Handler que nous utilisons pour démarrer le thread doit être une sous-classe de la classe Thread ou doit implémenter l interface Runnable. Le code utilisé dans l exemple est correct si la classe Handler est une sous-classe de la classe Thread. Si la classe Handler implémente au contraire l interface Runnable, le code de démarrage du thread devient alors le suivant : Thread thd = new Thread(new Handler(clientSock)); thd.start(); Voici l exemple d une classe Handler simple qui étend la classe Thread : class Handler extends Thread { Socket sock; Handler(Socket socket) { this.sock = socket;
Gérer plusieurs clients 137 public void run() { DataInputStream in = new DataInputStream(sock.getInputStream()); PrintStream out = new PrintStream(sock.getOutputStream(), true); // Gestion de la requête cliente sock.close(); Cette classe peut être utilisée pour gérer des requêtes clientes entrantes. L implémentation concrète de la gestion des requêtes clientes spécifiques n est pas définie dans cet exemple. Lorsque la méthode start() de cette classe est appelée, comme c est le cas dans notre exemple précédent, la méthode run() définie ici est exécutée. start() est implémentée dans la classe de base Thread : nous n avons pas à la redéfinir dans notre implémentation de Handler. Lors de la création d une solution multithread de ce type, il peut aussi être intéressant de créer un système de pooling des threads. Dans ce cas, vous créerez un pool de threads au démarrage de l application au lieu d engendrer un nouveau thread pour chaque requête entrante. Le pool de threads contient un nombre fixe de threads pouvant exécuter des tâches. Ce système évite que l application crée un nombre excessif de threads qui pourraient grever les performances système. Un très bon article (en anglais) concernant le pooling des threads peut être consulté à l adresse http://www.informit.com/articles/ article.asp?p=30483&seqnum=3&rl=1. Pour plus d informations sur l utilisation des threads, consultez le Chapitre 15, Utilisation de threads.
138 CHAPITRE 11 Serveurs réseau Servir du contenu HTTP Socket client = serversocket.accept(); BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); // Avant de servir une réponse, on lit habituellement // l entrée cliente et on traite la requête. PrintWriter out = new PrintWriter(client.getOutputStream()); out.println( HTTP/1.1 200 ); out.println( Content-Type: text/html ); String html = <html><head><title>test Response + </title></head><body>just a test</body></html> ; out.println( Content-length: + html.length()); out.println(html); out.flush(); out.close(); Cet exemple montre comment servir du contenu HTML très simple via HTTP. Pour commencer, nous acceptons une connexion avec un client, créons un BufferedReader pour lire la requête cliente et créons un PrintWriter que nous utilisons pour retransmettre le HTML via HTTP au client. Les données que nous écrivons vers le PrintWriter constituent le minimum nécessaire pour créer un message de réponse HTTP valide. Notre réponse est composée de trois champs d en-tête HTTP et de nos données HTML. Nous commençons notre réponse en spécifiant la version HTTP et un code de réponse dans la ligne suivante : out.println( HTTP/1.1 200 ); Nous indiquons le HTTP version 1.1 et le code de réponse 200. Ce code signale que la requête a réussi. À la ligne suivante, nous signalons que le type de contenu retourné est du HTML. D autres types de contenu peuvent être retournés pour un message de réponse HTTP valide.
Servir du contenu HTTP 139 Par exemple, la ligne suivante spécifie que la réponse correspond à du texte brut et non du code HTML : out.println( Content-Type: text/plain ); Ensuite, nous écrivons l en-tête Content-length. Celui-ci spécifie la longueur du contenu retourné, sans tenir compte des champs d en-tête. Après cela, nous écrivons le message HTML à retourner. Pour finir, nous purgeons le flux BufferedReader et le fermons avec les méthodes flush() et close(). Info Cette technique est utile pour les besoins simples en matière de service HTTP, mais il n est pas recommandé d écrire de toutes pièces un serveur HTTP complet en Java. Un excellent serveur HTTP, gratuit et open-source, est disponible : le serveur Tomcat. Pour plus d informations sur Tomcat et pour en télécharger les fichiers, accédez à tomcat.apache.org. Tomcat sert du contenu sur HTTP mais fournit également un conteneur de servlets pour gérer les servlets Java et les JSP.