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 ChatClient
classe si estende Frame
; questo è tipico di un'applicazione grafica. Implementiamo l' Runnable
interfaccia in modo da poter avviare una Thread
che 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 ChatClient
comunica 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 TextArea
dall'output e TextField
dall'input. Disegniamo e mostriamo la finestra e avviamo un Thread
listener 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 String
i messaggi dal flusso di input. Quando String
arriva un , lo aggiungiamo alla regione di output e ripetiamo il ciclo. Un IOException
potrebbe 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 EOFException
dal readUTF()
metodo.
Per ripulire, assegniamo prima il riferimento del nostro ascoltatore a questo Thread
a 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 OutputStream
o per assicurarci che la connessione sia chiusa.
Nota che eseguiamo tutta la pulizia in una finally
clausola, quindi questo accadrà se si IOException
verifica 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 IOException
verifica 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 Socket
all'host e alla porta specificati e creiamo un ChatClient
connesso 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 String
in 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 ChatHandler
classe 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 ChatHandler
classe) per ogni client.
Ogni nuovo ChatClient
si collegherà a ChatServer
; questo ChatServer
passerà la connessione a una nuova istanza della ChatHandler
classe che riceverà i messaggi dal nuovo client. All'interno della ChatHandler
classe, viene mantenuto un elenco dei gestori correnti; il broadcast()
metodo utilizza questo elenco per trasmettere un messaggio a tutti ChatClient
i 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 ServerSocket
e poi ci sediamo in un ciclo accettando i clienti con il accept()
metodo di ServerSocket
. Per ogni connessione, creiamo una nuova istanza della ChatHandler
classe, passando la nuova Socket
come 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, String
s.
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 ChatHandler
s 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 ChatHandler
s 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 Enumeration
degli attuali gestori. La Enumeration
classe 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.