Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Секреты программирования для Internet на Java

.pdf
Скачиваний:
181
Добавлен:
02.05.2014
Размер:
3.59 Mб
Скачать

while(true) {

Socket client_socket = server_port.accept(); Connection c = new Connection(client_socket,

CurrentConnections, 3, watcher, writer);

// избегаем одновременного доступа synchronized (connections) { connections.addElement(c); connection_list.addItem(c.getInfo());

}

}

}

catch (IOException e) fail(e, "Exception while listening for connections");

}

// запускаем сервер, прослушивающий определенный порт public static void main(String[] args) {

int port = 0;

if (args.length == 1) {

try port = Integer.parseInt(args[0]); catch (NumberFormatException e) port = 0;

}

new chatserver(port);

}

}

На этом месте наш сервер начал работу и слушает на порту 6001 новые связи. Для каждой новой связи сервер создает свой поток, называемый Connection, и передает ему соответствующие параметры. Мы добавляем новый поток в вектор, содержащий все потоки действующих связей. Этот вектор используется впоследствии для проверки состояния связи. SereverWriter также использует его для записи в присоединенные клиенты. Новое соединение добавляется в список, представленный во фрейме.

Общение с помощью сокетов и работа с потоками ввода/вывода

Класс Connection является потоком, осуществляющим все вводные операции с клиентом. Этот класс передает драйвер выходящего потока классу ServerWriter, потому что мы предназначили этот поток для записи данных присоединенных клиентов. Класс Connection инициализирует входной и выходной потоки присоединением соответствующих входных и выходных компонентов сокета. Кроме того, поскольку именно этот поток первым начинает работу конкретно для данной связи, в этом методе мы получаем начальное имя пользователя. Имя пользователя мы передаем классу chatserver, использующему метод getInfo.

Пример 15-1b. Программа сервера общения. class Connection extends Thread {

static int numberOfConnections = 0; protected Socket client;

protected ConnectionWatcher watcher; protected DataInputStream in; protected PrintStream out; protected ServerWriter writer;

public Connection(Socket client_socket, ThreadGroup CurrentConnections, int priority, ConnectionWatcher watcher, ServerWriter writer)

{

// присваиваем потоку имя и номер группы

super(CurrentConnections, "Connection number" + numberOfConnections++); this.setPriority(priority);

//задаем приоритет потока

//локализация параметров client = client_socket; this.watcher = watcher; this.writer = writer;

try {

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

in = new DataInputStream(client.getInputStream()); out = new PrintStream(client.getOutputStream()); writer.OutputStreams.addElement(out);

}

//Присоединяем потоки данных к входным и выходным потокам данных

//сокета клиента и добавляем outputstream к вектору, содержащему все

//выходные потоки данных, которые использует записывающий поток. catch (IOException e) {

try client.close(); catch (IOException e2) ; System.err.println("Exception while getting socket streams: "

+e);

return;

}

// запускаем поток на выполнение this.start();

}

//метод run выполняет цикл по чтению строк до тех пор,

//пока не прекратит работу из-за обрыва связи

public void run() { String inline;

out.println("Welcome to Internet Turbo Chat"); // посылаем приглашение клиенту

try {

// выполняем цикл до тех пор, пока связь не порвется while(true) {

// чтение строки inline = in.readLine();

if (inline == null) break;

// если null - связь порвалась writer.outdata.push(inline);

//сохраняем строку для записывающего потока synchronized(writer)writer.notify();

//И вызываем записывающий поток. Заметим, что synchronized() применяется для того,

//чтобы предотвратить одновременное обращение к записывающему потоку

//двух связей, что представляет собой форму блокировки.

}

}

catch (IOException e);

//Когда связь прервалась, производим очистку и вызываем watcher,

//чтобы убрать связь из вектора действующих связей и из списка.

//Watcher, кроме того, убирает outputstream из вектора записывающего потока,

//содержащего outputstreams.

finally {

try client.close(); catch (IOException e2) ;

synchronized (watcher) watcher.notify();

}

}

//Этот метод получает имя пользователя на начальной связи, печатает его для всех

//присоединенных в данный момент клиентов и передает обратно информацию для

//помещения ее в список текущих связей.

public String getInfo() { String inline="";

try { inline = in.readLine(); }

catch (IOException e) System.out.println("Caught an Exception:" +e);

writer.outdata.push(inline+" has joined chat\n"); synchronized(writer)writer.notify();

//Опять вызываем записывающий поток для представления сообщения нового

//пользователя. Делайте это синхронно, чтобы не получить множественный доступ.

return (inline+ " connected from: " + client.getInetAddress().getHostName());

www.books-shop.com

// возвращаем имя клиента и имя его компьютера для добавления в список связей

}

}

Теперь у нас есть входной читающий поток, определенный и запущенный для каждого присоединенного клиента. Читая новое сообщение клиента, мы вызываем записывающий поток для записи этого сообщения всем остальным клиентам. При этом мы легко осуществляем выход из клиента общения, вызвав watcher и дав ему задание убрать клиента, с которым прервалась связь, из всех векторов хранения.

Работа со многими связями и клиент множественного апплета

Мы рассмотрели два основных рабочих инструмента - потоки ServerWriter и ConnectionWatcher, которые руководят связями и генерируют выходные данные входных сообщений для всех клиентов. Единственная задача потока ServerWriter - ждать, пока его не "позовут" через его извещение (notify), затем взять сообщение, которое нужно послать клиентам, и послать его. ConnectionWatcher побуждается к действию своим собственным извещением, но, кроме того, он просыпается и запускается каждые 10 секунд. Его задача - проверять прочность каждой связи и убирать связи, переставшие функционировать.

Пример 15-1c. Программа сервера общения. class ServerWriter extends Thread {

chatserver server;

public Vector OutputStreams; public FIFO outdata; private String outputline;

//Делаем OutputStreams и outdata

//общими для обеспечения удобства работы; это позволяет получить прямой доступ,

//чтобы добавлять или убирать соответственно outputstream или данные сообщения.

public ServerWriter(chatserver s) {

super("Server Writer");

// помещаем этот поток в родительский ThreadGroup и даем ему имя server = s;

OutputStreams = new Vector(); outdata = new FIFO(); this.start();

//поток начинает работать

}

public synchronized void run() { while(true) {

//Поток совершает бесконечный цикл, но на самом деле он запускается только тогда,

//когда условие ожидания задается заново с помощью извещения.

//Опять же, мы делаем это в синхронном блоке, чтобы запереть поток и предотвратить

//множественный доступ.

try this.wait(); catch (InterruptedException e) System.out.println("Caught an Interrupted Exception");

outputline = outdata.pop();

//Получаем сообщение в верхней части FIFO outdata, куда уведомляющий

//метод должен был добавить сообщение.

synchronized(server.connections) {

//Мы должны еще запереть поток watcher,

//чтобы он не пытался что-нибудь делать до окончания работы программы.

for(int i = 0; i < OutputStreams.size(); i++) { PrintStream out;

out = (PrintStream)OutputStreams.elementAt(i); out.println(outputline);

// Делаем итерации по outputstreams и печатаем сообщение в каждый outputstream.

}

www.books-shop.com

}

}

}

}

Суперкласс выполняет большую работу, он запускается каждый раз, как приходит новое сообщение от одного из клиентов. Кроме того, он запирает поток ConnectionWatcher: это необходимо делать для того, чтобы убедиться, что выходной поток данных до окончания работы не убран из вектора OutputStreams потока ServerWriter. Когда выясняется, что связь прервалась,

поток ConnectionWatcher изменяет вектор OutputStreams потока ServerWriter.

Пример 15-1d. Программа сервера общения. class ConnectionWatcher extends Thread {

protected chatserver server; protected ServerWriter writer;

protected ConnectionWatcher(chatserver s, ServerWriter writer) { super(s.CurrentConnections, "ConnectionWatcher");

// помещаем поток в родительский ThreadGroup и даем ему имя server = s;

this.writer = writer; this.start();

}

//Этот метод ждет извещения о существующих потоках и очищает списки.

//Этот метод синхронный, то есть перед запуском он запирает объект

this.

//Это необходимо для того, чтобы метод мог вызвать wait() на this.

//Даже если объекты Connection никогда не вызовут notify(), этот метод включается

//каждые пять секунд и на всякий случай проверяет все связи.

//Заметим также, что весь доступ к вектору Vector и к компоненту GUI

List

//производится тоже внутри синхронного блока. Это предохраняет класс

Server

//от добавления новой связи, пока мы убираем старую связь.

public synchronized void run() {

//Мы запираем этот поток, когда он запускается,

//чтобы не запретить множественный доступ.

while(true) {

// поток совершает бесконечный цикл

try this.wait(10000);

//Поток "запускается" каждые 20 секунд, чтобы

//убрать возможные несуществующие связи.

catch (InterruptedException e){ System.out.println("Caught an Interrupted

Exception");

}

synchronized(server.connections) { // Проходим по каждой связи.

for(int i = 0; i < server.connections.size(); i++) { Connection c;

c = (Connection)server.connections.elementAt(i);

if (!c.isAlive()) {

// Если связь больше не существует, убираем ее из Vector. server.connections.removeElementAt(i);

writer.outdata.push(server.connection_list.getItem(i)+" has

left chat\n"); synchronized(writer)writer.notify();

//Говорим другим клиентам, что данный пользователь вышел. server.connection_list.delItem(i);

//Наконец, убираем его из списка связей сервера.

i--;

//Мы должны уменьшить счетчик, поскольку

//мы только что уменьшили вектор связей на единицу.

www.books-shop.com

}

}

}

}

}

}

На этом мы завершили работу над программой для сервера. Воспользовавшись различными классами, содержащимися в Java API, мы создали вполне ошибкоустойчивый многопотоковый сервер. Этот сервер можно посмотреть в действии, воспользовавшись клиентом общения на http://www.vmedia.com/onlcomp/ java/chapter15/ChatClient.html. Теперь мы перейдем к клиенту общения. При его создании мы также будем использовать многопотоковость для получения асинхронного общения.

СОВЕТ Другой пример выполнения серверов на Java можно посмотреть в главе 19, "Как написать свой собственный сервер: планировщик встреч".

Построение клиента общения

Апплет клиента общения содержится в окне Web-броузера (рис. 15-2). Этот апплет автоматически соединяется с портом 6001 на том хосте, с которого загружается Web-страница. Мы открываем фрейм, чтобы пользователь мог ввести свое имя и представиться остальным пользователям; создаем два независимых потока - один для чтения из сети и второй для записи в сеть. В результате наш клиент получается действительно асинхронным, поскольку пользователь может вводить свое сообщение, в то время как на экране появляются новые сообщения от других пользователей.

Рис. 15.2.

Пример 15-2. Клиент общения import java.io.*;

www.books-shop.com

import java.net.*; import java.awt.*; import java.applet.*;

public class chatclient extends Applet { public static final int DEFAULT_PORT = 6001; public Socket socket;

private Thread reader, writer; public TextArea OutputArea; public TextField InputArea; public PrintStream out; public String Name;

public UserInfoFrame NameFrame;

// создаем читающий и записывающий потоки и запускаем их public void init () {

OutputArea = new TextArea(20, 45); InputArea = new TextField(45); NameFrame = new UserInfoFrame(this); add( new Label("Internet Turbo Chat")); add(OutputArea);

add( new Label("Type a message below and hit ENTER")); add(InputArea);

resize(400,400); try {

socket = new Socket(getDocumentBase().getHost(), DEFAULT_PORT);

reader = new Reader(this, OutputArea); out = new PrintStream(socket.getOutputStream());

// Задаем различные приоритеты, чтобы консоль разделялась эффективно. reader.setPriority(3);

reader.start();

}

catch (IOException e) System.err.println(e);

}

public boolean handleEvent(Event evt) { if (evt.target == InputArea)

{

char c=(char)evt.key; if (c == '\n')

//Ждем, пока пользователь нажмет клавишу ENTER.

//Это показывает нам, что сообщение готово к отправке.

{

String InLine = InputArea.getText(); out.println(Name + "> " + InLine); InputArea.setText("");

//Отправляем сообщение, но добавляем к нему имя пользователя,

//чтобы другие клиенты знали, кто послал сообщение.

return true;

}

}

else if ( evt.target == NameFrame) {

//Первое введенное имя пользователя передается базовому апплету.

//Мы должны послать это имя на сервер, чтобы оно задало список пользователей.

Name = (String)evt.arg; out.println(Name);

// посылаем имя пользователя на сервер общения return true;

}

return false;

}

}

//Читающий метод читает входные данные из сокета

//и обновляет OutputArea, вводя новое сообщение. class Reader extends Thread {

www.books-shop.com

protected chatclient client; private TextArea OutputArea;

public Reader(chatclient c, TextArea OutputArea) { super("chatclient Reader");

this.client = c; this.OutputArea = OutputArea;

}

public void run() { DataInputStream in = null; String line;

try {

in = new DataInputStream(client.socket.getInputStream());

while(true) {

// бесконечный цикл

line = in.readLine(); if (line == null) {

OutputArea.setText("Server closed

connection.");

break;

// цикл разрывается, когда связь прекратилась

}

OutputArea.appendText(line+"\n"); // добавляем новое сообщение к OutputArea

}

}

catch (IOException e) System.out.println("Reader: " + e); finally try if (in != null) in.close(); catch (IOException e)

;

System.exit(0);

}

}

//Это класс frame, который нужен, чтобы получить имя пользователя. class UserInfoFrame extends Frame {

public TextField UserNameField; public Applet parent;

public UserInfoFrame(Applet parent) { UserNameField = new TextField(10); this.parent=parent;

add("North", new Label("Please enter your name and hit ENTER")); add("South", UserNameField);

resize(300, 100); show();

}

//Передаем введенное имя в форме события, посланного апплету-родителю. public boolean keyDown( Event evt, int key)

{

char c=(char)key; if (c == '\n')

{

Event NewEvent = new Event(this,

Event.ACTION_EVENT, UserNameField.getText());

parent.postEvent(NewEvent); // генерируем событие на родительском апплете

dispose();

// теперь разрушаем фрейм

return true;

}

else { return false; } } // действие

}

www.books-shop.com

Что дальше?

Вы получили полную порцию основного блюда, теперь готовьтесь к десерту! Мы обрисовали в общих чертах и обсудили темы, важные при создании апплетов игр, построении систем клиентсервер, обладающих полным набором свойств, и почти все промежуточные темы. В части V мы предлагаем четыре учебных апплета, использующих принципы построения, технику и средства, описанные в этой книге. Желаем вам получить удовольствие!

Глава 16

Интерактивная анимация: рекламный апплет

Контракт

Свойства План работы

Создание структуры изображения Компоновка структуры изображения

Реализация Возможности конфигурации

Базовые классы для экранного вывода Создание анализатора

Создание ActionArea

Возможные улучшения

Что вы узнаете из этой главы

Здесь мы создадим интерактивный анимационный апплет высокого уровня. Создание анимаций и интерактивности само по себе является достаточно простой задачей,, но мы хотим написать апплет,, c которым легко будет работать Web-дизайнерам,, не имеющим средств программирования на Java. Рассмотрим вначале,, какие знания нам для этого понадобятся:

Использование классов URL для доступа к файлу конфигурации.

Динамическая загрузка удаленных классов с использованием класса Class.

Создание интерфейса для расширения возможностей конфигурации.

Трассировка изображений с помощью MediaTracker.

Применение двойной буферизации для устранения мерцания экрана.

Распределение событий мыши.

Контракт

www.books-shop.com

Представьте, что рекламное агентство, работающее в on-line, захотело разнообразить свою Web-страницу с помощью апплетов. Особенно оно заинтересовалось интерактивной анимацией, предоставляемой языком Java, и вас попросили создать апплет, который будет создавать анимацию из серии картинок, а также взаимодействовать с пользователем. Например, пользователь должен иметь возможность щелкнуть или переместить мышь над какой-то частью картинки, чтобы загрузить новую страницу или изменить анимацию. При этом, поскольку сотрудники агентства не могут программировать самостоятельно, требуется апплет высокого уровня, в котором задана возможность изменять картинки, составляющие анимацию, и смоделирована реакция апплета на действия пользователя.

Таким образом, у нашего апплета предполагаются две аудитории - пользователи Web, которые будут смотреть апплет, и Web-дизайнеры, которые будут его использовать для своих целей. Однако прежде чем углубиться в программирование, давайте посмотрим, как использовать для достижения желаемых целей то, что мы уже знаем. После этого мы построим и реализуем апплет высокого уровня.

Свойства

Мы знаем, что у нашего апплета будут две аудитории - пользователи Web и Web-дизайнеры из рекламного агентства. Давайте подумаем, какими свойствами должен обладать апплет, чтобы удовлетворить потребностям первой аудитории.

Цель апплета - оживить Web-страницу при передаче рекламного сообщения пользователю Web. Наш апплет - средство передачи сообщения, следовательно, мы должны быть уверены, что апплет правильно работает в качестве такого средства. Первая трудность, с которой мы столкнемся, состоит в том, что апплет должен быть загружен по сети и запущен. Поскольку он является только одним из элементов Web-страницы, люди могут не захотеть ждать, пока он появится перед глазами. Если загрузка будет занимать слишком много времени, человек может уйти со страницы еще до того, как он узнает о существовании апплета. Эта проблема особенно остра для такого апплета, как наш, потому что он должен загружать много картинок, а передача изображений по Интернет требует времени. Поэтому нужно сделать так, чтобы апплет запускался сразу же, еще до того, как загрузятся изображения.

Кроме того, мы должны быть уверены, что анимация изображений будет гладкой. Это достигается с помощью техники двойной буферизации, описанной в главе 5, "Апплет в работе".

Вопросы, как сделать быструю загрузку, гладкую анимацию и простой в использовании интерфейс, относятся к техническим аспектам функционирования апплета в качестве средства передачи сообщения. Теперь нам нужно подумать о том, как облегчить Web-дизайнеру процесс отображения сообщения. Мы не можем влиять на Web-дизайнера в его выборе графики для апплета, но мы можем сделать так, чтобы изображения легко встраивались в апплет.

Как обсуждалось в главе 5, Web-дизайнер может воспользоваться тегом <PARAM> для того, чтобы менять способ работы апплета. Но этого было бы достаточно, если бы мы описывали только серии изображений для анимации. Однако в нашем апплете Web-дизайнеру понадобится описывать не только области внутри каждой картинки, но также процесс взаимодействия изображений с пользователем. И вся эта информация должна содержаться в теге <APPLET>. А если Web-дизайнер захочет использовать ту же конфигурацию для другой Web-страницы, потребуется переносить части текста с одной страницы на другую.

Такой перенос текста может быть очень утомительным, поэтому мы будем хранить конфигурационную информацию в отдельном файле. Тег param-value будет использоваться только для указания на конфигурационный файл. Этот файл будет написан на очень простом языке программирования, который мы разработаем специально для этого апплета. Тут мы, конечно, легко можем увлечься, в результате чего Web-дизайнеру придется потом изучать сложный и разнообразный синтаксис. Но лучше не допустить этого, ведь наш заказчик хочет, чтобы апплет был формируемым и простым для использования.

Мы говорили о двух основных аудиториях, для которых предназначен апплет, - Webпользователи и Web-дизайнеры. Давайте теперь рассмотрим, что мы - программисты - можем получить от опыта создания апплета. Мы воспользуемся техникой, описанной в главе 10, "Структура программы", чтобы убедиться, что у нас есть компоненты для неоднократного использования, и построим апплет таким образом, чтобы потом в него легко было добавлять новые функции.

План работы

Ну что ж, проблемы, связанные с нашим будущим апплетом, мы уже обсудили, давайте обдумывать решение. Представим себе наш апплет с точки зрения внешней и внутренней сторон. Внешняя сторона апплета - это та часть, которую видит пользователь, а внутренняя - это то, что

www.books-shop.com

использует Web-дизайнер для формирования апплета. Внешняя сторона - это логическая отправная точка, поскольку лишь создав нечто, можно думать о его формировании.

Создание структуры изображения

Мы уже знаем, как обращаться со многими основными элементами нашего изображения, умеем анимировать картинки и осуществлять взаимодействие с мышью. По сути, все, что мы делаем, состоит из этих двух элементов. Когда мы перемещаем мышь или щелкаем мышью по картинке, мы смотрим на области, описанные для данного изображения, и, если мышь находится внутри одной из них, выполняем соответствующее действие.

Давайте на время оставим анимацию и подумаем о том, как выделить интерактивную часть нашего апплета. Первой нашей задачей будет выяснение того, находится ли мышь внутри области, описанной Web-дизайнером. Беглый обзор электронной документации по Abstract Windows Toolkit показывает, что в нашем распоряжении имеется класс Polygon. Кроме того, AWT содержит методы, позволяющие определить, находится ли данная точка внутри фигуры. Так что нам не придется писать сложные алгоритмы для определения местоположения мыши; для этого достаточно создать реализацию классов Polygon и позволить им делать необходимые расчеты.

Следующей задачей будет задание правильной реакции на перемещение или щелкание мыши внутри одной из определенных областей. Одно из решений - создание длинного ряда операторов if-then-else в методах mouseMove и mouseDown. Мы можем при конфигурации апплета загрузить каждую из наших областей и соответствующих ей действий в хеш-таблицу и при каждом действии мыши смотреть в таблицу. Будем считать, что мы используем классы Polygon только для описания областей и поместим их всех в хеш-таблицу под названием areasTable. Тогда метод mouseDown может обходиться с отвечающими на щелчок мыши областями следующим образом:

public boolean mouseDown(Event evt, int x, int y) { Enumeration e=areasTable.keys();

while (e.hasMoreElements()) { Polygon p=e.getNextElement(); if (p.inside(x,y)) {

String S=(String)areasTable((Polygon(p)); if (S.equals("sound action"))

// издаем звук

if (s.equals("link action")) // связь со страницей

if (s.equals("redirect action"))

// меняем анимацию

//...и т. д., для всех типов областей

}

return true;

}

В нашем примере возникает несколько проблем, связанных с эффективностью работы программы, которых можно избежать. Однако, сделав программу более эффективной, мы можем сузить область ее применения. Если мы будем пользоваться методом просмотра таблицы, нам придется дописывать программу каждый раз при возникновении нового типа действия. Вместо этого можно создать базовый класс для активных областей. Тогда наш апплет будет просто говорить активной области делать то, что она должна, и метод mouseDown будет гораздо проще. Поскольку нам больше не нужна хеш-таблица, будем хранить наши области actionArea в векторе aAreas:

public boolean mouseDown(Event evt, int x, int y) { Enumeration e=aAreas.elements();

while (e.hasMoreElements()) { actionArea A=e.NextElement(); if (A.inside(x,y))

A.doAction();

}

Теперь можно не переписывать апплет каждый раз, когда мы создаем новую активную область. Кроме того, метод mouseDown стал гораздо яснее. Но что если мы захотим создать две одновременные анимации или добавить апплету дополнительные функции? Проблема в том, что мы поместили все функции непосредственно во внутренние методы апплета.

www.books-shop.com