Aufbau eines Internet-Chat-Systems

Möglicherweise haben Sie eines der vielen Java-basierten Chat-Systeme gesehen, die im Web aufgetaucht sind. Nachdem Sie diesen Artikel gelesen haben, werden Sie verstehen, wie sie funktionieren - und wissen, wie Sie ein einfaches Chat-System erstellen.

Dieses einfache Beispiel eines Client / Server-Systems soll zeigen, wie Anwendungen nur mit den in der Standard-API verfügbaren Streams erstellt werden. Der Chat verwendet TCP / IP-Sockets für die Kommunikation und kann problemlos in eine Webseite eingebettet werden. Als Referenz bieten wir eine Seitenleiste mit Erläuterungen zu Java-Netzwerkprogrammierkomponenten, die für diese Anwendung relevant sind. Wenn Sie immer noch auf dem neuesten Stand sind, schauen Sie sich zuerst die Seitenleiste an. Wenn Sie sich bereits mit Java auskennen, können Sie direkt hineinspringen und einfach in der Seitenleiste nachschlagen.

Erstellen eines Chat-Clients

Wir beginnen mit einem einfachen grafischen Chat-Client. Es sind zwei Befehlszeilenparameter erforderlich - der Servername und die Portnummer, zu der eine Verbindung hergestellt werden soll. Es stellt eine Socket-Verbindung her und öffnet dann ein Fenster mit einem großen Ausgabebereich und einem kleinen Eingabebereich.

Die ChatClient-Oberfläche

Nachdem der Benutzer Text in den Eingabebereich eingegeben und die Eingabetaste gedrückt hat, wird der Text an den Server übertragen. Der Server gibt alles zurück, was vom Client gesendet wird. Der Client zeigt alles an, was vom Server im Ausgabebereich empfangen wurde. Wenn mehrere Clients eine Verbindung zu einem Server herstellen, verfügen wir über ein einfaches Chat-System.

Klasse ChatClient

Diese Klasse implementiert den Chat-Client wie beschrieben. Dies umfasst das Einrichten einer grundlegenden Benutzeroberfläche, das Behandeln der Benutzerinteraktion und das Empfangen von Nachrichten vom Server.

import java.net. *; import java.io. *; import java.awt. *; public class ChatClient erweitert Frame implementiert Runnable {// public ChatClient (String title, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) löst eine IOException aus ...}

Die ChatClientKlasse erweitert sich Frame; Dies ist typisch für eine grafische Anwendung. Wir implementieren die RunnableSchnittstelle, damit wir eine starten können Thread, die Nachrichten vom Server empfängt. Der Konstruktor führt die Grundeinstellungen der GUI durch, die run()Methode empfängt Nachrichten vom Server, die handleEvent()Methode übernimmt die Benutzerinteraktion und die main()Methode führt die anfängliche Netzwerkverbindung durch.

geschützter DataInputStream i; geschützter DataOutputStream o; geschützte TextArea-Ausgabe; geschützte TextField-Eingabe; geschützter Thread-Listener; öffentlicher ChatClient (String-Titel, InputStream i, OutputStream o) {super (Titel); this.i = neuer DataInputStream (neuer BufferedInputStream (i)); this.o = neuer DataOutputStream (neuer BufferedOutputStream (o)); setLayout (neues BorderLayout ()); add ("Center", output = new TextArea ()); output.setEditable (false); add ("South", input = new TextField ()); Pack (); Show (); input.requestFocus (); Listener = neuer Thread (this); listener.start (); }}

Der Konstruktor verwendet drei Parameter: einen Titel für das Fenster, einen Eingabestream und einen Ausgabestream. Der ChatClientkommuniziert über die angegebenen Streams; Wir erstellen gepufferte Datenströme i und o, um effiziente Kommunikationsmöglichkeiten auf höherer Ebene über diese Ströme bereitzustellen. Anschließend richten wir unsere einfache Benutzeroberfläche ein, die aus der TextAreaAusgabe und der TextFieldEingabe besteht. Wir gestalten und zeigen das Fenster und starten einen ThreadListener, der Nachrichten vom Server akzeptiert.

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

Wenn der Listener-Thread in die Run-Methode eintritt, sitzen wir in einer Endlosschleife und lesen Strings aus dem Eingabestream. Wenn ein Stringankommt, hängen wir es an den Ausgabebereich an und wiederholen die Schleife. Ein IOExceptionkann auftreten, wenn die Verbindung zum Server unterbrochen wurde. In diesem Fall drucken wir die Ausnahme aus und führen eine Bereinigung durch. Beachten Sie, dass dies durch eine EOFExceptionvon der readUTF()Methode signalisiert wird .

Um aufzuräumen, weisen wir zuerst unseren Hörer-Verweis darauf Threadzu null; Dies zeigt dem Rest des Codes an, dass der Thread beendet wurde. Wir verstecken dann das Eingabefeld und rufen auf, validate()damit die Schnittstelle wieder angelegt wird, und schließen das OutputStreamo, um sicherzustellen, dass die Verbindung geschlossen ist.

Beachten Sie, dass wir die gesamte Bereinigung in einer finallyKlausel durchführen. Dies geschieht also unabhängig davon, ob hier eine IOExceptionauftritt oder der Thread zwangsweise gestoppt wird. Wir schließen das Fenster nicht sofort; Es wird davon ausgegangen, dass der Benutzer die Sitzung möglicherweise auch dann lesen möchte, wenn die Verbindung unterbrochen wurde.

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

In der handleEvent()Methode müssen wir nach zwei signifikanten UI-Ereignissen suchen:

Das erste ist ein Aktionsereignis in der TextField, was bedeutet, dass der Benutzer die Eingabetaste gedrückt hat. Wenn wir dieses Ereignis abfangen, schreiben wir die Nachricht in den Ausgabestream und rufen dann auf, flush()um sicherzustellen, dass sie sofort gesendet wird. Der Ausgabestream ist a DataOutputStream, also können wir a writeUTF()senden String. Wenn eine IOExceptionauftritt, muss die Verbindung fehlgeschlagen sein, daher stoppen wir den Listener-Thread. Dadurch werden automatisch alle erforderlichen Bereinigungen durchgeführt.

Das zweite Ereignis ist der Benutzer, der versucht, das Fenster zu schließen. Es ist Sache des Programmierers, sich um diese Aufgabe zu kümmern. Wir stoppen den Listener-Thread und verstecken den Frame.

public static void main (String args []) löst IOException aus {if (args.length! = 2) löst eine neue RuntimeException aus ("Syntax: ChatClient"); Socket s = neuer Socket (args [0], Integer.parseInt (args [1])); neuer ChatClient ("Chat" + Argumente [0] + ":" + Argumente [1], s.getInputStream (), s.getOutputStream ()); }}

Die main()Methode startet den Client. Wir stellen sicher, dass die richtige Anzahl von Argumenten angegeben wurde, öffnen a Socketfür den angegebenen Host und Port und erstellen eine ChatClientVerbindung zu den Streams des Sockets. Das Erstellen des Sockets kann eine Ausnahme auslösen, die diese Methode beendet und angezeigt wird.

Erstellen eines Multithread-Servers

We now develop a chat server that can accept multiple connections and that will broadcast everything it reads from any client. It is hardwired to read and write Strings in UTF format.

There are two classes in this program: the main class, ChatServer, is a server that accepts connections from clients and assigns them to new connection handler objects. The ChatHandler class actually does the work of listening for messages and broadcasting them to all connected clients. One thread (the main thread) handles new connections, and there is a thread (the ChatHandler class) for each 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.

Innerhalb dieses synchronisierten Blocks erhalten wir einen Enumerationder aktuellen Handler. Die EnumerationKlasse bietet eine bequeme Möglichkeit, alle Elemente von a zu durchlaufen Vector. Unsere Schleife schreibt einfach die Nachricht in jedes Element der Enumeration. Beachten Sie, dass ChatClientwir die stop()Methode des Clients aufrufen , wenn beim Schreiben in a eine Ausnahme auftritt . Dadurch wird der Thread des Clients gestoppt und daher die entsprechende Bereinigung durchgeführt, einschließlich des Entfernens des Clients aus den Handlern.