Изграждане на система за интернет чат

Може би сте виждали една от многото базирани на Java системи за чат, които се появиха в мрежата. След като прочетете тази статия, ще разберете как работят - и ще знаете как да изградите проста система за чат.

Този прост пример за клиент / сървърна система има за цел да демонстрира как да изграждате приложения, използвайки само потоците, налични в стандартния API. Чатът използва TCP / IP сокети за комуникация и може лесно да се вгради в уеб страница. За справка предлагаме странична лента, обясняваща компонентите за мрежово програмиране на Java, които са от значение за това приложение. Ако все още набирате скорост, първо разгледайте страничната лента. Ако вече сте добре запознати с Java, можете да влезете направо и просто да се обърнете към страничната лента за справка.

Изграждане на клиент за чат

Започваме с прост графичен клиент за чат. Необходими са два параметъра на командния ред - името на сървъра и номера на порта, за да се свържете. Той прави връзка с гнездо и след това отваря прозорец с голям изходен регион и малък входен регион.

Интерфейсът ChatClient

След като потребителят въведе текст в областта за въвеждане и натисне Return, текстът се предава на сървъра. Сървърът отеква обратно всичко, което е изпратено от клиента. Клиентът показва всичко получено от сървъра в изходния регион. Когато множество клиенти се свързват с един сървър, ние имаме проста система за чат.

Class ChatClient

Този клас изпълнява чат клиента, както е описано. Това включва настройка на основен потребителски интерфейс, обработка на взаимодействие с потребителя и получаване на съобщения от сървъра.

импортиране на java.net. *; импортиране на java.io. *; импортиране на java.awt. *; публичен клас ChatClient разширява Frame реализира Runnable {// публичен ChatClient (заглавие на низ, InputStream i, OutputStream o) ... // public void run () ... // public boolean handleEvent (Event e) ... // public static void main (String args []) хвърля IOException ...}

В ChatClientкласа простира Frame; това е типично за графично приложение. Ние прилагаме Runnableинтерфейса, за да можем да стартираме, Threadкойто получава съобщения от сървъра. Конструкторът извършва основната настройка на GUI, run()методът получава съобщения от сървъра, handleEvent()методът се справя с потребителското взаимодействие и main()методът извършва първоначалната мрежова връзка.

защитен DataInputStream i; защитен DataOutputStream o; защитен изход TextArea; защитен въвеждане на TextField; защитен слушател на нишки; публичен ChatClient (заглавие на низ, InputStream i, OutputStream o) {супер (заглавие); this.i = нов DataInputStream (нов BufferedInputStream (i)); this.o = нов DataOutputStream (нов BufferedOutputStream (o)); setLayout (нов BorderLayout ()); add ("Center", output = new TextArea ()); output.setEditable (false); add ("South", input = new TextField ()); пакет (); покажи (); input.requestFocus (); listener = нова тема (това); listener.start (); }

Конструкторът взема три параметъра: заглавие за прозореца, входен поток и изходен поток. На ChatClientкомуникира през определени потоци; ние създаваме буферирани потоци от данни i и o, за да осигурим ефективни средства за комуникация на по-високо ниво над тези потоци. След това настройваме нашия прост потребителски интерфейс, състоящ се от TextAreaизхода и TextFieldвхода. Оформляваме и показваме прозореца и стартираме Threadслушател, който приема съобщения от сървъра.

public void run () {try {while (true) {String line = i.readUTF (); output.appendText (ред + "\ n"); }} catch (IOException ex) {ex.printStackTrace (); } накрая {listener = null; input.hide (); валидиране (); опитайте {o.close (); } catch (IOException ex) {ex.printStackTrace (); }}}

Когато нишката на слушателя влезе в метода за изпълнение, ние седим в безкраен цикъл, отчитащ Strings от входния поток. Когато Stringпристигне, ние го добавяме към изходния регион и повтаряме цикъла. Може IOExceptionда възникне, ако връзката със сървъра е загубена. В този случай ние разпечатваме изключението и извършваме почистване. Имайте предвид, че това ще бъде сигнализирано от EOFExceptionот readUTF()метода.

За да се почисти, за първи път се възлага нашия слушател позоваване на настоящия Threadдо null; това показва на останалата част от кода, че нишката е прекратена. След това скриваме полето за въвеждане и се обаждаме, validate()така че интерфейсът да е разположен отново, и затваряме OutputStreamo, за да гарантираме, че връзката е затворена.

Имайте предвид, че ние извършваме цялото почистване в finallyклауза, така че това ще се случи, независимо дали се IOExceptionпоявява тук или нишката е спряна принудително. Не затваряме прозореца веднага; предположението е, че потребителят може да иска да прочете сесията дори след загубата на връзката.

публичен булев handleEvent (Събитие 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 (""); връщане вярно; } else if ((e.target == this) && (e.id == Event.WINDOW_DESTROY)) {if (listener! = null) listener.stop (); Крия (); връщане вярно; } връщане super.handleEvent (e); }

В handleEvent()метода трябва да проверим за две значими UI събития:

Първото е събитие за действие в TextField, което означава, че потребителят е натиснал клавиша Return. Когато уловим това събитие, ние записваме съобщението в изходния поток, след което се обаждаме, за flush()да гарантираме, че то е изпратено незабавно. Изходният поток е a DataOutputStream, така че можем да използваме writeUTF()за изпращане на a String. Ако се IOExceptionслучи, връзката трябва да е неуспешна, затова спираме нишката на слушателя; това автоматично ще извърши всички необходими почиствания.

Второто събитие е опитът на потребителя да затвори прозореца. Програмистът трябва да се погрижи за тази задача; спираме нишката на слушателя и скриваме Frame.

public static void main (String args []) хвърля IOException {if (args.length! = 2) хвърля нов RuntimeException ("Синтаксис: ChatClient"); Socket s = нов Socket (аргументи [0], Integer.parseInt (аргументи [1])); нов ChatClient ("Чат" + аргументи [0] + ":" + аргументи [1], s.getInputStream (), s.getOutputStream ()); }

В main()метод започват на клиента; ние гарантираме, че е предоставен правилният брой аргументи, отваряме a Socketкъм посочения хост и порт и създаваме ChatClientсвързан към потоците на сокета. Създаването на сокета може да доведе до изключение, което ще излезе от този метод и ще бъде показано.

Изграждане на многонишков сървър

Сега разработваме чат сървър, който може да приема множество връзки и който ще излъчва всичко, което чете от всеки клиент. Това е твърдо свързване за четене и писане Stringв UTF формат.

В тази програма има два класа: основният клас, ChatServerе сървър, който приема връзки от клиенти и ги присвоява на нови обекти на манипулатора на връзки. В ChatHandlerкласа всъщност върши работата на слушане за съобщения и ги излъчвате към всички свързани клиенти. Една нишка (основната нишка) обработва нови връзки и има нишка ( ChatHandlerклас) за всеки клиент.

Всяко ново ChatClientще се свърже с ChatServer; това ChatServerще предаде връзката към нов екземпляр на ChatHandlerкласа, който ще получава съобщения от новия клиент. В рамките на ChatHandlerкласа се поддържа списък на текущите манипулатори; на broadcast()метода използва този списък, за да предаде съобщение до всички свързани ChatClientс.

Клас ChatServer

Този клас се занимава с приемане на връзки от клиенти и стартиране на нишки на манипулатора за тяхната обработка.

импортиране на java.net. *; импортиране на java.io. *; импортиране на java.util. *; публичен клас ChatServer {// публичен ChatServer (int порт) хвърля IOException ... // публичен статичен void main (String args []) хвърля IOException ...}

Този клас е просто самостоятелно приложение. Доставяме конструктор, който изпълнява цялата действителна работа за класа, и main()метод, който действително го стартира.

публичен ChatServer (int порт) хвърля IOException {ServerSocket сървър = нов ServerSocket (порт); while (true) {Socket client = server.accept (); System.out.println ("Прието от" + client.getInetAddress ()); ChatHandler c = нов ChatHandler (клиент); c.start (); }}

Този конструктор, който изпълнява цялата работа на сървъра, е доста прост. Създаваме ServerSocketи след това седим в цикъл, приемайки клиенти с accept()метода на ServerSocket. За всяка връзка създаваме нов екземпляр на ChatHandlerкласа, предавайки новия Socketкато параметър. След като сме създали този манипулатор, ние го стартираме с неговия start()метод. Това стартира нова нишка за обработка на връзката, за да може основният ни сървърен цикъл да продължи да чака при нови връзки.

public static void main (String args []) хвърля IOException {if (args.length! = 1) хвърля нов RuntimeException ("Синтаксис: ChatServer"); нов ChatServer (Integer.parseInt (аргументи [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.

В рамките на този синхронизиран блок получаваме един Enumerationот текущите манипулатори. Най- Enumerationкласа предлага удобен начин, за да превъртите през всички елементи на една Vector. Нашият цикъл просто записва съобщението във всеки елемент на Enumeration. Имайте предвид, че ако възникне изключение по време на запис в a ChatClient, тогава ние извикваме stop()метода на клиента ; това спира нишката на клиента и следователно извършва подходящото почистване, включително премахване на клиента от манипулатори.