Construire un système de chat Internet

Vous avez peut-être vu l'un des nombreux systèmes de discussion basés sur Java qui ont surgi sur le Web. Après avoir lu cet article, vous comprendrez comment ils fonctionnent et vous saurez comment créer votre propre système de chat simple.

Cet exemple simple de système client / serveur est destiné à montrer comment créer des applications en utilisant uniquement les flux disponibles dans l'API standard. Le chat utilise des sockets TCP / IP pour communiquer et peut être intégré facilement dans une page Web. Pour référence, nous fournissons une barre latérale expliquant les composants de programmation de réseau Java qui sont pertinents pour cette application. Si vous êtes toujours au courant, jetez d'abord un œil à la barre latérale. Si vous êtes déjà familiarisé avec Java, vous pouvez vous lancer directement et vous référer simplement à la barre latérale pour référence.

Créer un client de chat

Nous commençons avec un simple client de chat graphique. Il faut deux paramètres de ligne de commande - le nom du serveur et le numéro de port auquel se connecter. Il établit une connexion socket, puis ouvre une fenêtre avec une grande région de sortie et une petite région d'entrée.

L'interface ChatClient

Une fois que l'utilisateur a tapé du texte dans la zone d'entrée et a frappé Retour, le texte est transmis au serveur. Le serveur renvoie tout ce qui est envoyé par le client. Le client affiche tout ce qui est reçu du serveur dans la région de sortie. Lorsque plusieurs clients se connectent à un serveur, nous avons un système de chat simple.

Classe ChatClient

Cette classe implémente le client de discussion, comme décrit. Cela implique la mise en place d'une interface utilisateur de base, la gestion de l'interaction utilisateur et la réception de messages du serveur.

import java.net. *; import java.io. *; import java.awt. *; classe publique ChatClient étend Frame implements Runnable {// public ChatClient (String title, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) lève IOException ...}

La ChatClientclasse s'étend Frame; c'est typique pour une application graphique. Nous implémentons l' Runnableinterface afin de pouvoir démarrer un Threadqui reçoit des messages du serveur. Le constructeur effectue la configuration de base de l'interface graphique, la run()méthode reçoit les messages du serveur, la handleEvent()méthode gère l'interaction de l'utilisateur et la main()méthode effectue la connexion réseau initiale.

protégé DataInputStream i; protégé DataOutputStream o; sortie TextArea protégée; entrée TextField protégée; écouteur Thread protégé; public ChatClient (titre de chaîne, InputStream i, OutputStream o) {super (titre); this.i = new DataInputStream (nouveau BufferedInputStream (i)); this.o = new DataOutputStream (nouveau BufferedOutputStream (o)); setLayout (nouveau BorderLayout ()); add ("Center", output = new TextArea ()); output.setEditable (faux); add ("Sud", entrée = nouveau TextField ()); pack (); spectacle (); input.requestFocus (); listener = new Thread (this); listener.start (); }

Le constructeur prend trois paramètres: un titre pour la fenêtre, un flux d'entrée et un flux de sortie. Le ChatClientcommunique sur les flux spécifiés; nous créons des flux de données tamponnés i et o pour fournir des installations de communication de haut niveau efficaces sur ces flux. Nous avons ensuite mis en place notre interface utilisateur simple, composée de la TextAreasortie et de l' TextFieldentrée. Nous mettons en page et montrons la fenêtre et démarrons un Threadécouteur qui accepte les messages du serveur.

public void run () {try {while (true) {String line = i.readUTF (); output.appendText (ligne + "\ n"); }} catch (IOException ex) {ex.printStackTrace (); } enfin {auditeur = null; input.hide (); valider (); essayez {o.close (); } catch (IOException ex) {ex.printStackTrace (); }}}

Lorsque le thread d'écoute entre dans la méthode run, nous nous asseyons dans une boucle infinie lisant Strings à partir du flux d'entrée. Quand un Stringarrive, nous l'ajoutons à la région de sortie et répétons la boucle. Un IOExceptionpeut se produire si la connexion au serveur a été perdue. Dans ce cas, nous imprimons l'exception et effectuons le nettoyage. Notez que cela sera signalé par un EOFExceptionde la readUTF()méthode.

Pour nettoyer, nous attribuons d'abord notre référence d'auditeur à ceci Threadà null; cela indique au reste du code que le thread s'est terminé. Nous masquons ensuite le champ de saisie et appelons validate()pour que l'interface soit à nouveau disposée, et fermons le OutputStreamo pour nous assurer que la connexion est fermée.

Notez que nous effectuons tout le nettoyage dans une finallyclause, donc cela se produira si un IOExceptionse produit ici ou si le thread est arrêté de force. Nous ne fermons pas la fenêtre immédiatement; l'hypothèse est que l'utilisateur peut vouloir lire la session même après la perte de la connexion.

public boolean handleEvent (Event e) {if ((e.target == input) && (e.id == Event.ACTION_EVENT)) {try {o.writeUTF ((String) e.arg); o.flush (); } catch (IOException ex) {ex.printStackTrace (); listener.stop (); } input.setText (""); retourne vrai; } else if ((e.target == this) && (e.id == Event.WINDOW_DESTROY)) {if (listener! = null) listener.stop (); cacher (); retourne vrai; } return super.handleEvent (e); }

Dans la handleEvent()méthode, nous devons vérifier deux événements d'interface utilisateur importants:

Le premier est un événement d'action dans le TextField, ce qui signifie que l'utilisateur a appuyé sur la touche Retour. Lorsque nous captons cet événement, nous écrivons le message dans le flux de sortie, puis appelons flush()pour nous assurer qu'il est envoyé immédiatement. Le flux de sortie est a DataOutputStream, nous pouvons donc l'utiliser writeUTF()pour envoyer un fichier String. Si un IOExceptionse produit, la connexion doit avoir échoué, nous arrêtons donc le thread d'écoute; cela effectuera automatiquement tous les nettoyages nécessaires.

Le deuxième événement est l'utilisateur qui tente de fermer la fenêtre. C'est au programmeur de s'occuper de cette tâche; nous arrêtons le fil d'écoute et masquons le fichier Frame.

public static void main (String args []) lance IOException {if (args.length! = 2) throw new RuntimeException ("Syntaxe: ChatClient"); Socket s = nouveau Socket (args [0], Integer.parseInt (args [1])); nouveau ChatClient ("Chat" + args [0] + ":" + args [1], s.getInputStream (), s.getOutputStream ()); }

La main()méthode démarre le client; nous nous assurons que le nombre correct d'arguments a été fourni, nous ouvrons un Socketà l'hôte et au port spécifiés, et nous créons un ChatClientconnecté aux flux de la socket. La création du socket peut lever une exception qui quittera cette méthode et s'affichera.

Construire un serveur multithread

Nous développons maintenant un serveur de chat qui peut accepter plusieurs connexions et qui diffusera tout ce qu'il lit à partir de n'importe quel client. Il est câblé pour lire et écrire des Strings au format UTF.

Il existe deux classes dans ce programme: la classe principale,, ChatServerest un serveur qui accepte les connexions des clients et les assigne à de nouveaux objets de gestionnaire de connexion. La ChatHandlerclasse fait en fait le travail d'écouter les messages et de les diffuser à tous les clients connectés. Un thread (le thread principal) gère les nouvelles connexions et il existe un thread (la ChatHandlerclasse) pour chaque client.

Every new ChatClient will connect to the ChatServer; this ChatServer will hand the connection to a new instance of the ChatHandler class that will receive messages from the new client. Within the ChatHandler class, a list of the current handlers is maintained; the broadcast() method uses this list to transmit a message to all connected ChatClients.

Class ChatServer

This class is concerned with accepting connections from clients and launching handler threads to process them.

import java.net.*; import java.io.*; import java.util.*; public class ChatServer { // public ChatServer (int port) throws IOException ... // public static void main (String args[]) throws IOException ... } 

This class is a simple standalone application. We supply a constructor that performs all of the actual work for the class, and a main() method that actually starts it.

 public ChatServer (int port) throws IOException { ServerSocket server = new ServerSocket (port); while (true) { Socket client = server.accept (); System.out.println ("Accepted from " + client.getInetAddress ()); ChatHandler c = new ChatHandler (client); c.start (); } } 

This constructor, which performs all of the work of the server, is fairly simple. We create a ServerSocket and then sit in a loop accepting clients with the accept() method of ServerSocket. For each connection, we create a new instance of the ChatHandler class, passing the new Socket as a parameter. After we have created this handler, we start it with its start() method. This starts a new thread to handle the connection so that our main server loop can continue to wait on new connections.

public static void main (String args[]) throws IOException { if (args.length != 1) throw new RuntimeException ("Syntax: ChatServer "); new ChatServer (Integer.parseInt (args[0])); } 

The main() method creates an instance of the ChatServer, passing the command-line port as a parameter. This is the port to which clients will connect.

Class ChatHandler

This class is concerned with handling individual connections. We must receive messages from the client and re-send these to all other connections. We maintain a list of the connections in a

static

Vector.

import java.net.*; import java.io.*; import java.util.*; public class ChatHandler extends Thread { // public ChatHandler (Socket s) throws IOException ... // public void run () ... } 

We extend the Thread class to allow a separate thread to process the associated client. The constructor accepts a Socket to which we attach; the run() method, called by the new thread, performs the actual client processing.

 protected Socket s; protected DataInputStream i; protected DataOutputStream o; public ChatHandler (Socket s) throws IOException { this.s = s; i = new DataInputStream (new BufferedInputStream (s.getInputStream ())); o = new DataOutputStream (new BufferedOutputStream (s.getOutputStream ())); } 

The constructor keeps a reference to the client's socket and opens an input and an output stream. Again, we use buffered data streams; these provide us with efficient I/O and methods to communicate high-level data types -- in this case, Strings.

protected static Vector handlers = new Vector (); public void run () { try { handlers.addElement (this); while (true) { String msg = i.readUTF (); broadcast (msg); } } catch (IOException ex) { ex.printStackTrace (); } finally { handlers.removeElement (this); try { s.close (); } catch (IOException ex) { ex.printStackTrace(); } } } // protected static void broadcast (String message) ... 

The run() method is where our thread enters. First we add our thread to the Vector of ChatHandlers handlers. The handlers Vector keeps a list of all of the current handlers. It is a static variable and so there is one instance of the Vector for the whole ChatHandler class and all of its instances. Thus, all ChatHandlers can access the list of current connections.

Note that it is very important for us to remove ourselves from this list afterward if our connection fails; otherwise, all other handlers will try to write to us when they broadcast information. This type of situation, where it is imperative that an action take place upon completion of a section of code, is a prime use of the try ... finally construct; we therefore perform all of our work within a try ... catch ... finally construct.

The body of this method receives messages from a client and rebroadcasts them to all other clients using the broadcast() method. When the loop exits, whether because of an exception reading from the client or because this thread is stopped, the finally clause is guaranteed to be executed. In this clause, we remove our thread from the list of handlers and close the socket.

protected static void broadcast (String message) { synchronized (handlers) { Enumeration e = handlers.elements (); while (e.hasMoreElements ()) { ChatHandler c = (ChatHandler) e.nextElement (); try { synchronized (c.o) { c.o.writeUTF (message); } c.o.flush (); } catch (IOException ex) { c.stop (); } } } } 

This method broadcasts a message to all clients. We first synchronize on the list of handlers. We don't want people joining or leaving while we are looping, in case we try to broadcast to someone who no longer exists; this forces the clients to wait until we are done synchronizing. If the server must handle particularly heavy loads, then we might provide more fine-grained synchronization.

Dans ce bloc synchronisé, nous obtenons un Enumerationdes gestionnaires actuels. La Enumerationclasse fournit un moyen pratique de parcourir tous les éléments d'un fichier Vector. Notre boucle écrit simplement le message dans chaque élément du Enumeration. Notez que si une exception se produit lors de l'écriture dans a ChatClient, nous appelons la stop()méthode du client ; cela arrête le thread du client et effectue donc le nettoyage approprié, y compris la suppression du client des gestionnaires.