Costruire un sistema di chat su Internet

Potresti aver visto uno dei tanti sistemi di chat basati su Java che sono apparsi sul Web. Dopo aver letto questo articolo, capirai come funzionano e saprai come creare un tuo semplice sistema di chat.

Questo semplice esempio di un sistema client / server ha lo scopo di dimostrare come creare applicazioni utilizzando solo i flussi disponibili nell'API standard. La chat utilizza socket TCP / IP per comunicare e può essere facilmente incorporata in una pagina Web. Per riferimento, forniamo una barra laterale che spiega i componenti di programmazione di rete Java rilevanti per questa applicazione. Se ti stai ancora allenando, dai un'occhiata prima alla barra laterale. Se sei già esperto in Java, però, puoi saltare subito e fare semplicemente riferimento alla barra laterale per riferimento.

Costruire un client di chat

Iniziamo con un semplice client di chat grafico. Richiede due parametri della riga di comando: il nome del server e il numero di porta a cui connettersi. Effettua una connessione socket e quindi apre una finestra con una grande regione di output e una piccola regione di input.

L'interfaccia ChatClient

Dopo che l'utente ha digitato il testo nella regione di input e ha premuto Invio, il testo viene trasmesso al server. Il server restituisce tutto ciò che viene inviato dal client. Il client visualizza tutto ciò che è stato ricevuto dal server nella regione di output. Quando più client si connettono a un server, abbiamo un semplice sistema di chat.

Classe ChatClient

Questa classe implementa il client di chat, come descritto. Ciò implica la configurazione di un'interfaccia utente di base, la gestione dell'interazione dell'utente e la ricezione di messaggi dal server.

import java.net. *; import java.io. *; import java.awt. *; public class ChatClient estende Frame implementa Runnable {// public ChatClient (String title, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) genera IOException ...}

La ChatClientclasse si estende Frame; questo è tipico di un'applicazione grafica. Implementiamo l' Runnableinterfaccia in modo da poter avviare una Threadche riceve i messaggi dal server. Il costruttore esegue la configurazione di base della GUI, il run()metodo riceve i messaggi dal server, il handleEvent()metodo gestisce l'interazione dell'utente e il main()metodo esegue la connessione di rete iniziale.

DataInputStream i protetto; DataOutputStream protetto o; output TextArea protetto; input TextField protetto; listener di thread protetto; Public ChatClient (String title, InputStream i, OutputStream o) {super (title); this.i = new DataInputStream (new BufferedInputStream (i)); this.o = new DataOutputStream (new BufferedOutputStream (o)); setLayout (new BorderLayout ()); add ("Center", output = new TextArea ()); output.setEditable (false); add ("South", input = new TextField ()); pack (); spettacolo (); input.requestFocus (); listener = new Thread (this); listener.start (); }

Il costruttore accetta tre parametri: un titolo per la finestra, un flusso di input e un flusso di output. Il ChatClientcomunica sui flussi specificati; creiamo flussi di dati bufferizzati i e o per fornire servizi di comunicazione di livello superiore efficienti su questi flussi. Abbiamo quindi impostato la nostra semplice interfaccia utente, composta TextAreadall'output e TextFielddall'input. Disegniamo e mostriamo la finestra e avviamo un Threadlistener che accetta i messaggi dal server.

public void run () {try {while (true) {String line = i.readUTF (); output.appendText (riga + "\ n"); }} catch (IOException ex) {ex.printStackTrace (); } infine {listener = null; input.hide (); validate (); prova {o.close (); } catch (IOException ex) {ex.printStackTrace (); }}}

Quando il thread del listener entra nel metodo run, ci troviamo in un ciclo infinito che legge Stringi messaggi dal flusso di input. Quando Stringarriva un , lo aggiungiamo alla regione di output e ripetiamo il ciclo. Un IOExceptionpotrebbe verificarsi se la connessione al server è stata persa. In tal caso, stampiamo l'eccezione ed eseguiamo la pulizia. Notare che questo verrà segnalato da un EOFExceptiondal readUTF()metodo.

Per ripulire, assegniamo prima il riferimento del nostro ascoltatore a questo Threada null; questo indica al resto del codice che il thread è terminato. Quindi nascondiamo il campo di input e chiamiamo in validate()modo che l'interfaccia sia nuovamente disposta e chiudiamo OutputStreamo per assicurarci che la connessione sia chiusa.

Nota che eseguiamo tutta la pulizia in una finallyclausola, quindi questo accadrà se si IOExceptionverifica qui o il thread viene interrotto forzatamente. Non chiudiamo subito la finestra; il presupposto è che l'utente possa voler leggere la sessione anche dopo che la connessione è stata persa.

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 (""); restituire vero; } else if ((e.target == this) && (e.id == Event.WINDOW_DESTROY)) {if (listener! = null) listener.stop (); nascondere (); restituire vero; } return super.handleEvent (e); }

Nel handleEvent()metodo, dobbiamo verificare due eventi significativi dell'interfaccia utente:

Il primo è un evento di azione nel TextField, il che significa che l'utente ha premuto il tasto Invio. Quando rileviamo questo evento, scriviamo il messaggio nel flusso di output, quindi chiamiamo flush()per assicurarci che venga inviato immediatamente. Il flusso di output è un DataOutputStream, quindi possiamo usare writeUTF()per inviare un file String. Se si IOExceptionverifica un errore, la connessione deve essere fallita, quindi interrompiamo il thread del listener; questo eseguirà automaticamente tutte le operazioni di pulizia necessarie.

Il secondo evento è l'utente che tenta di chiudere la finestra. Spetta al programmatore occuparsi di questo compito; fermiamo il thread dell'ascoltatore e nascondiamo il file Frame.

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

Il main()metodo avvia il client; ci assicuriamo che sia stato fornito il numero corretto di argomenti, apriamo un Socketall'host e alla porta specificati e creiamo un ChatClientconnesso ai flussi del socket. La creazione del socket potrebbe generare un'eccezione che uscirà da questo metodo e verrà visualizzata.

Costruire un server multithread

Ora sviluppiamo un server di chat che può accettare più connessioni e che trasmetterà tutto ciò che legge da qualsiasi client. È cablato per leggere e scrivere Stringin formato UTF.

Ci sono due classi in questo programma: la classe principale,, ChatServerè un server che accetta connessioni dai client e le assegna a nuovi oggetti gestore di connessione. La ChatHandlerclasse esegue effettivamente il lavoro di ascolto dei messaggi e di trasmetterli a tutti i client collegati. Un thread (il thread principale) gestisce le nuove connessioni e c'è un thread (la ChatHandlerclasse) per ogni client.

Ogni nuovo ChatClientsi collegherà a ChatServer; questo ChatServerpasserà la connessione a una nuova istanza della ChatHandlerclasse che riceverà i messaggi dal nuovo client. All'interno della ChatHandlerclasse, viene mantenuto un elenco dei gestori correnti; il broadcast()metodo utilizza questo elenco per trasmettere un messaggio a tutti ChatClienti messaggi collegati .

Classe ChatServer

Questa classe si occupa di accettare le connessioni dai client e di avviare i thread del gestore per elaborarle.

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

Questa classe è una semplice applicazione standalone. Forniamo un costruttore che esegue tutto il lavoro effettivo per la classe e un main()metodo che lo avvia effettivamente.

public ChatServer (int port) genera IOException {ServerSocket server = new ServerSocket (port); while (vero) {Socket client = server.accept (); System.out.println ("Accettato da" + client.getInetAddress ()); ChatHandler c = nuovo ChatHandler (client); c.start (); }}

Questo costruttore, che esegue tutto il lavoro del server, è abbastanza semplice. Creiamo un ServerSockete poi ci sediamo in un ciclo accettando i clienti con il accept()metodo di ServerSocket. Per ogni connessione, creiamo una nuova istanza della ChatHandlerclasse, passando la nuova Socketcome parametro. Dopo aver creato questo gestore, lo iniziamo con il suo start()metodo. Ciò avvia un nuovo thread per gestire la connessione in modo che il loop del nostro server principale possa continuare ad attendere nuove connessioni.

public static void main (String args []) genera IOException {if (args.length! = 1) genera una nuova RuntimeException ("Sintassi: ChatServer"); nuovo 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.

All'interno di questo blocco sincronizzato otteniamo uno Enumerationdegli attuali gestori. La Enumerationclasse offre un modo conveniente per scorrere tutti gli elementi di un file Vector. Il nostro ciclo scrive semplicemente il messaggio in ogni elemento del file Enumeration. Si noti che se si verifica un'eccezione durante la scrittura in a ChatClient, viene chiamato il stop()metodo del client ; ciò arresta il thread del client e quindi esegue la pulizia appropriata, inclusa la rimozione del client dai gestori.