Budowa systemu czatu internetowego

Być może widzieliście jeden z wielu opartych na Javie systemów rozmów, które pojawiły się w sieci. Po przeczytaniu tego artykułu zrozumiesz, jak działają - i wiesz, jak zbudować własny prosty system czatu.

Ten prosty przykład systemu klient / serwer ma na celu zademonstrowanie, jak budować aplikacje przy użyciu tylko strumieni dostępnych w standardowym API. Czat wykorzystuje gniazda TCP / IP do komunikacji i można go łatwo osadzić na stronie internetowej. W celach informacyjnych udostępniamy pasek boczny wyjaśniający komponenty programowania sieciowego Java, które są istotne dla tej aplikacji. Jeśli nadal nabierasz tempa, najpierw spójrz na pasek boczny. Jeśli jednak jesteś już dobrze zorientowany w Javie, możesz wskoczyć od razu i po prostu odnieść się do paska bocznego w celach informacyjnych.

Tworzenie klienta czatu

Zaczynamy od prostego graficznego klienta do czatu. Wymaga dwóch parametrów wiersza poleceń - nazwy serwera i numeru portu, z którym chcesz się połączyć. Tworzy połączenie przez gniazdo, a następnie otwiera okno z dużym obszarem wyjściowym i małym obszarem wejściowym.

Interfejs ChatClient

Po wpisaniu tekstu przez użytkownika w obszarze wejściowym i naciśnięciu klawisza Return, tekst jest przesyłany na serwer. Serwer odbija wszystko, co jest wysyłane przez klienta. Klient wyświetla wszystko otrzymane z serwera w regionie wyjściowym. Kiedy wielu klientów łączy się z jednym serwerem, mamy prosty system czatu.

Class ChatClient

Ta klasa implementuje klienta czatu, zgodnie z opisem. Obejmuje to skonfigurowanie podstawowego interfejsu użytkownika, obsługę interakcji użytkownika i odbieranie wiadomości z serwera.

import java.net. *; import java.io. *; import java.awt. *; public class ChatClient rozszerza Frame implementuje Runnable {// public ChatClient (String title, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) zgłasza wyjątek IOException ...}

ChatClientKlasa obejmuje Frame; jest to typowe dla aplikacji graficznej. Implementujemy Runnableinterfejs tak, aby można było uruchomić stronę Threadodbierającą komunikaty z serwera. Konstruktor wykonuje podstawową konfigurację GUI, run()metoda odbiera komunikaty z serwera, handleEvent()metoda obsługuje interakcję użytkownika, a main()metoda wykonuje początkowe połączenie sieciowe.

chroniony DataInputStream i; chroniony DataOutputStream o; chronione wyjście TextArea; chronione wejście TextField; chroniony odbiornik wątku; 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 (nowy BorderLayout ()); add ("Centrum", wyjście = nowy TextArea ()); output.setEditable (false); add ("Południe", input = new TextField ()); Pakiet (); pokazać (); input.requestFocus (); listener = nowy wątek (ten); listener.start (); }

Konstruktor przyjmuje trzy parametry: tytuł okna, strumień wejściowy i strumień wyjściowy. W ChatClientkomunikuje się z określonych strumieni; tworzymy buforowane strumienie danych i i o, aby zapewnić wydajną komunikację wyższego poziomu przez te strumienie. Następnie skonfigurowaliśmy nasz prosty interfejs użytkownika, składający się z danych TextAreawyjściowych i TextFielddanych wejściowych. Układamy i pokazujemy okno oraz uruchamiamy Threadodbiornik, który akceptuje wiadomości z serwera.

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

Kiedy wątek nasłuchujący wchodzi do metody run, siedzimy w nieskończonej pętli odczytującej Strings ze strumienia wejściowego. Kiedy Stringnadejdzie a , dołączamy go do regionu wyjściowego i powtarzamy pętlę. IOExceptionMogłyby wystąpić, jeśli połączenie z serwerem zostało przerwane. W takim przypadku drukujemy wyjątek i wykonujemy czyszczenie. Zauważ, że zostanie to zasygnalizowane przez EOFExceptionod readUTF()metody.

Oczyścić, najpierw przypisać nasze odniesienie słuchacza to Threaddo null; wskazuje to pozostałej części kodu, że wątek został zakończony. Następnie ukrywamy pole wejściowe i wywołujemy, validate()aby interfejs został ponownie ułożony, i zamykamy OutputStreamo, aby upewnić się, że połączenie jest zamknięte.

Zauważ, że wszystkie porządki wykonujemy w finallyklauzuli, więc będzie to miało miejsce niezależnie od tego, czy IOExceptionwystąpi tutaj, czy wątek zostanie siłą zatrzymany. Nie zamykamy okna od razu; zakłada się, że użytkownik może chcieć czytać sesję nawet po utracie połączenia.

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 (""); powrót prawda; } else if ((e.target == this) && (e.id == Event.WINDOW_DESTROY)) {if (listener! = null) listener.stop (); ukryć (); powrót prawda; } return super.handleEvent (e); }

W handleEvent()metodzie musimy sprawdzić dwa istotne zdarzenia interfejsu użytkownika:

Pierwsza to zdarzenie akcji w tagu TextField, co oznacza, że ​​użytkownik nacisnął klawisz Return. Kiedy złapiemy to zdarzenie, piszemy wiadomość do strumienia wyjściowego, a następnie wywołujemy, flush()aby upewnić się, że zostanie wysłana natychmiast. Strumień wyjściowy to a DataOutputStream, więc możemy użyć writeUTF()do wysłania pliku String. Jeśli IOExceptionwystąpi, połączenie musiało się nie udać, więc zatrzymujemy wątek nasłuchujący; spowoduje to automatyczne wykonanie wszystkich niezbędnych czynności porządkowych.

Drugie zdarzenie to próba zamknięcia okna przez użytkownika. Zadanie to zależy od programisty; zatrzymujemy wątek słuchacza i ukrywamy plik Frame.

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

main()Metoda rozpoczyna klienta; upewniamy się, że Socketpodano prawidłową liczbę argumentów, otwieramy do określonego hosta i portu oraz tworzymy ChatClientpodłączenie do strumieni gniazda. Utworzenie gniazda może spowodować zgłoszenie wyjątku, który zakończy działanie tej metody i zostanie wyświetlony.

Budowa serwera wielowątkowego

Obecnie opracowujemy serwer czatu, który może akceptować wiele połączeń i nadawać wszystko, co odczytuje od dowolnego klienta. Jest podłączony do odczytu i zapisu Stringw formacie UTF.

W tym programie są dwie klasy: główna klasa ChatServerto serwer, który akceptuje połączenia od klientów i przypisuje je do nowych obiektów obsługi połączeń. ChatHandlerKlasa faktycznie działa słuchania komunikatów i nadawanie im do wszystkich podłączonych klientów. Jeden wątek (główny wątek) obsługuje nowe połączenia, a ChatHandlerdla każdego klienta istnieje wątek ( klasa).

Każdy nowy ChatClientpołączy się z ChatServer; ChatServerspowoduje to przekazanie połączenia do nowej instancji ChatHandlerklasy, która będzie otrzymywać komunikaty od nowego klienta. W ChatHandlerklasie utrzymywana jest lista aktualnych programów obsługi; broadcast()metoda wykorzystuje tę listę, aby przesłać wiadomość do wszystkich podłączonych ChatClients.

Class ChatServer

Ta klasa zajmuje się akceptowaniem połączeń od klientów i uruchamianiem wątków obsługi w celu ich przetwarzania.

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 ...}

Ta klasa to prosta samodzielna aplikacja. Dostarczamy konstruktora, który wykonuje całą rzeczywistą pracę dla klasy oraz main()metodę, która faktycznie ją uruchamia.

public ChatServer (port int) rzuca IOException {ServerSocket server = new ServerSocket (port); while (true) {Socket client = server.accept (); System.out.println ("Zaakceptowano z" + client.getInetAddress ()); ChatHandler c = nowy ChatHandler (klient); c.start (); }}

Ten konstruktor, który wykonuje całą pracę serwera, jest dość prosty. Tworzymy, ServerSocketa następnie siedzimy w pętli przyjmując klientów accept()metodą ServerSocket. Dla każdego połączenia tworzymy nową instancję ChatHandlerklasy, przekazując nową Socketjako parametr. Po utworzeniu tego handlera zaczynamy od jego start()metody. To uruchamia nowy wątek do obsługi połączenia, tak aby nasza główna pętla serwera mogła nadal czekać na nowe połączenia.

public static void main (String args []) throws IOException {if (args.length! = 1) throw new RuntimeException ("Składnia: ChatServer"); nowy 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.

W tym zsynchronizowanym bloku otrzymujemy Enumerationz bieżących programów obsługi. EnumerationKlasa zapewnia wygodny sposób wykonać iterację wszystkich elementów Vector. Nasza pętla po prostu zapisuje wiadomość w każdym elemencie Enumeration. Zauważ, że jeśli wystąpi wyjątek podczas pisania do a ChatClient, wówczas wywołujemy stop()metodę klienta ; To zatrzymuje wątek klienta i dlatego wykonuje odpowiednie czyszczenie, w tym usuwanie klienta z programów obsługi.