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

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

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

whiteTime.setFont(fixed);

whiteTime.setAlignment(Label.CENTER); whitePlayer.add("whitename;fill=x;pady=5", whiteName); whitePlayer.add("whitetime;fill=x;pady=5", whiteTime); blackPlayer = new Panel();

blackPlayer.setLayout(new PackerLayout()); blackName = new Label("-------"); blackName.setFont(fixed); blackName.setAlignment(Label.CENTER); blackTime = new stopWatch(); blackTime.setFont(fixed); blackTime.setAlignment(Label.CENTER);

blackPlayer.add("blackname;fill=x;pady=5", blackName); blackPlayer.add("blacktime;fill=x;pady=5", blackTime); players = new Panel();

players.setLayout(new PackerLayout()); players.add("wplayer;side=bottom;fill=x", whitePlayer); players.add("bplayer;side=top;fill=x", blackPlayer); add("panel1;side=left", players);

board = new ChessBoard(); add("panel2;side=left", board); pack();

resize(size());

show();

}

Данный кадр обрабатывает лишь одно событие - WINDOWS_DESTROY. Любое другое событие перенаправляется к родительскому объекту. Таким образом, события, связанные с ходами игроков, передаются выше и в конце концов попадают на сервер:

public boolean handleEvent(Event evt) {

if (evt.id==Event.WINDOWS_DESTROY) { dispose();

return true; } else {

parent.deliverEvent(evt); return true;

}

}

В момент первоначального обновления шахматной доски мы обнаруживаем, какого цвета фигурами играет наш игрок. Если он играет черными, метод flipPlayers поменяет метки игроков местами и перевернет доску:

protected void flipPlayers() { players.remove(whitePlayer); players.remove(blackPlayer); players.add("wplayer;side=top;fill=x" whitePlayer);

players.add("bplayer;side=bottom;fill=x", blackPlayer); board.setOrientation(false);

}

Следующий метод является сердцевиной объекта ChessFrame - он занимается разбором сообщения об обновлении. Сперва при помощи StringTokenizer мы считываем из этого сообщения восемь строк состояния шахматной доски. Далее, мы определяем, чей был ход (эта информация будет использована немного позже, когда мы будем определять, сколько времени осталось у каждого игрока). Вот фрагмент кода, реализующего этот алгоритм:

public void updateGame(String s) { System.out.println(s);

StringTokenizer st = new StringTokenizer(s); st.nextToken(); // метка-индикатор 12 String lines[] = new String[8];

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

for (int i=0; i; i++) {

lines[i] = st.nextToken();

}

String token = st.nextToken(); // Чей это ход (W, B) boolean whiteMove = true;

if (token.equals("B")) { whiteMove = false;

}

st.nextToken();

st.nextToken(); // могут ли белые делать короткую рокировку? st.nextToken(); // могут ли белые делать длинную рокировку? st.nextToken(); // могут ли черные делать короткую рокировку? st.nextToken(); // могут ли черные делать длинную рокировку? st.nextToken(); // количество ходов

st.nextToken(); // номер игры

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

token = st.nextToken() // имя игрока белыми if (! WhiteName.getText().equals(token)) {

whiteName.setText(token);

}

token = st.nextToken(); // имя игрока черными String type = st.nextToken();

if (! BlackName.getText().equals(token)) { blackName.setText(token);

if (user.equals(token) && ! type.equals("2")) { board.setOrientation(false); flipPlayers();

System.out.prinln("Tried to anyway");

}

layout();

}

if (type.equals("1") || type.equals("2")) { board.setGenerateMoves(true);

} else { board.setGenerateMoves(false);

}

token = st.nextToken(); // время начала партии token = st.nextToken();

token = st.nextToken(); token = st.nextToken();

Два следующих токена содержат количество секунд, оставшееся в распоряжении каждого из игроков. StopWatches устанавливается на новое, правильное время, для игрока, который только что сделал ход, отсчет останавливается и, в свою очередь, запускается счетчик другого игрока:

token = st.nextToken(); // оставшееся время игрока белыми try {

Integer i = new Integer(token); whiteTime.set(i.intValue());

} catch (NumberFormatException e) {}

token = st.nextToken(); // оставшееся время игрока черными try {

Integer i = new Integer(token); blackTime.set(i.intValue());

www.books-shop.com

}catch (NumberFormatException e) {} if (whiteMove) {

blackTime.stop();

whiteTime.start();

}else {

whiteTime.stop();

blackTime.start();

}

И наконец, мы приступаем к разбору оставшихся токенов. Для того чтобы процедура обновления шахматной доски происходила незаметно для пользователя, мы выполняем ее в самый последний момент. Считывание каждой строки состояния происходит последовательно, символ за символом. Если очередной считанный символ является дефисом, соответствующий квадрат доски не содержит картинки; в противном случае картинка запрашивается у родительского процесса:

for (int i=0; i; i++) {

String line = lines[i]; char ary [] = new char[8]; line.getChar(0, 8, ary, 0); for (int j=0; j; j++) {

char row = (char)(`8'-i); char column = (char)(`a'+j); Image piece;

if (ary[j]!='-`) {

piece = parent.getPieceImage(""+ary[j]); } else {

piece = null;

}

board.setImage(piece,""+column+row);

}

}

Написание апплета

Мы создали отдельное окно, в котором расположена игровая доска общего назначения и информация, необходимая игроку. Теперь мы можем приступить к написанию собственно апплета. Мы начнем с малого, добавляя дополнительные функции по мере необходимости. Первая реализация апплета должна уметь соединяться с сервером и предлагать игроку окно, позволяющее выбрать партнера из списка. Как только принимается строка, содержащая в начале <12>, апплет должен передавать ее классу ChessFrame, конструируя последний в случае необходимости. Кроме того, апплет должен различать сигнал сервера о конце игры и передавать эту информацию в ChessFrame. Вот исходный текст апплета ChessClient:

import ChessFrame; import PackerLayout; import java.applet.*; import java.awt.*; import java.net.*; import java.util.*; import java.io.*; import ventana.io.*;

public class ChessClient extends Applet implements Listener { private String user;

private Label title; private TextArea output; private TextField input; private Hashtable games;

private Hashtable pieceImages; privateSocket ChessSocket; private InputStream ChessInput;

private OutputStream ChessOutput;

private InputStreamHandler ChessInputHandler; private MediaTracker tracker;

www.books-shop.com

private Font fixed; private Font pretty;

Первое, что необходимо сделать при инициализации апплета в процедуре init, - установить два типа шрифтов, pretty и fixed, для всего апплета. Поскольку на разных компьютерах разрешение экрана сильно варьируется, шрифт размером в десять пунктов, выглядящий вполне пристойно на старом мониторе невысокого разрешения, может оказаться совершенно нечитаемым на современном мониторе высокого разрешения. По этой причине вопрос о выборе размера шрифта остается на усмотрение разработчика HTML-страницы. Для установки шрифтов используется следующая последовательность команд:

public void init() {

String fixedName = getParameter("FIXEDFONTNAME"); if (fixedName==null) {

fixedName = "Courier";

}

int fixedSize = 12;

String fixedSizestr = getParameter("FIXEDFONTSIZE"); if (fixedSizestr!=null) {

try {

Integer i = new Integer(fixedSizestr); fixedSize = i.intValue();

} catch (NumberFormatException e) {}

}

fixed = new Font(fixedName, Font.PLAIN, fixedSize); String prettyName = getParameter("PRETTYFONTNAME"); if (prettyName==null) {

prettyName = "Courier";

}

int prettySize = 12;

String prettySizestr = getParameter("PRETTYFONTSIZE"); if (prettySizestr!=null) {

try {

Integer i = new Integer(prettySizestr); prettySize = i.intValue();

} catch (NumberFormatException e) {}

}

pretty = new Font(prettyName, Font.PLAIN, prettySize);

Теперь, когда мы установили информацию о шрифтах, пора приступать непосредственно к апплету. Мы добавляем метку-заголовок TextArea для отображения данных, поступивших с сервера. Кроме того, мы загружаем изображения фигур в хеш-таблицу:

setLayout(new PackerLayout());

title = new Label("Internet Chess Server"); title.setFont(

new Font(prettyName, Font.PLAIN, prettySize+12)); title.setAlignment(Label.CENTER); add("title;side=top;fill=x", title);

output = new TextArea(20, 80); output.setEditable(false); output.setFont(fixed); add("output;side=top;fill=x", output); input = new TextField(80); input.setFont(fixed); add("input;side=top;fill=x", input); show();

pieceImages = new Hashtable(10); initPieceImages();

}

Данная процедура создает хеш-таблицу объетов Image. Символ отображает тип фигуры, которой соответствует данный объект Image. Вначале таблица индексирована в порядке URL (в

www.books-shop.com

формате String). Далее мы проходим по таблице, извлекаем все изображения, на которые ссылаются URL, и строим из них новую хеш-таблицу. Для того чтобы до завершения работы все изображения были гарантированно извлечены, используется объект MediaTracker:

protected void initPieceImages() { tracker = new MediaTracker(this); HashTable h = new HashTable(10); h.put("r", "pics/br.gif"); h.put("n", "pics/bn.gif"); h.put("b", "pics/bb.gif"); h.put("q", "pics/bq.gif"); h.put("k", "pics/bk.gif"); h.put("p", "pics/bp.gif"); h.put("P", "pics/wp.gif"); h.put("R", "pics/wr.gif"); h.put("N", "pics/wn.gif"); h.put("B", "pics/wb.gif"); h.put("Q", "pics/wq.gif"); h.put("K", "pics/wk.gif"); Enumeration e = h.keys(); while(e.hasMoreElements()) {

try {

String key = (String)e.nextElement(); String s = (String)g.get(key);

URL u = new URL(getCodeBase(), s); Image i = getImage(u); tracker.addImage(u); pieceImages.put(key, i);

} catch (Ecxeption ex) { handleException(ex);

}

}

try {tracker.waitForAll();} catch (InterruptedException ex) {}

}

Метод getPieceImage возвращает объект типа Image, на который указывает фигура String. Например, вызов getPieceImage("P") возвращает объект Image, соответствующий белой пешке:

public Image getPieceImage(String piece) { return (Image)pieceImages.get(piece);

}

Стартовав, апплет устанавливает соединение с сервером через сокет. Для объекта InputStream создается соответствующий InputStreamHandler. Как вы помните, данный класс считывает строки из входного потока и передает их с помощью метода receiveInput. Наконец, создается хештаблица для хранения объектов ChessFrame. Большинство пользователей предпочитают играть или наблюдать за одной игрой в один момент времени, поэтому хеш-таблица инициализируется для хранения одного элемента:

public void start() { try {

String ChessHost = hetParameter("HOST"); if (ChessHost==null) {

ChessHost = getCodeBase().getHost();

}

ChessSocket = new Socket(ChessHost, 5000); ChessInput = ChessSocket.getInputStream(); ChessOutput = ChessSocket.getOutputStream();

ChessInputHandler = new InputStreamHandler(ChessInput, this); } catch (Exception e) {

handleException(e);

}

games = new Hashtable(1);

www.books-shop.com

Заканчивая работу, апплет закрывает InputStreamHandler, сетевые потоки и сокет, а также очищает окна ввода и отображения информации:

public void stop() { try {

ChessInputHandler.close();

ChessOutput.close();

ChessInput.close();

ChessSocket.close(); } catch (IOException e);

handleException(e);

}

output.setText("");

input.setText("");

}

public void handleException(Exception e) { e.printStackTrace();

}

Для передачи данных серверу используется метод writeOutput. То, что данный метод - единственный предназначенный для передачи данных, дает нам два преимущества: во-первых, нам не нужно повторять один и тот же набор операторов в каждом месте программы, где нужно выводить данные, во-вторых, поскольку в методе использовано ключевое слово synchronized, у нас есть гарантия, что несколько потоков программы не начнут одновременную передачу. В начале переменной user еще не присвоено значение, однако оно автоматически присваивается, как только сделан первый вызов writeOutput. Так происходит потому, что первая команда, подающаяся на сервер, является именем пользователя:

protected synchronized void writeOutput(String s) { if (user==null) {

user = s.trim();

}

byte b[] = new byte[s.length()]; s.getBytes(0, s.length(), b, 0); try {

ChessOutput.write(b); } catch (IOException e) {

handleException(e);

}

}

Метод receiveInput вызывается объектом InputStreamHandler каждый раз, как только получена полная входная строка. Если принятая строка пуста или содержит только приглашение сервера, она полностью игнорируется. Если принятая строка начинается с символов <12>, мы знаем, что она является сигналом к обновлению состояния игры - вызывается метод parseBoard. Если похоже, что строка является сигналом к завершению игры, анализируется ее номер. Если номер игры входит в нашу хеш-таблицу, вызывается метод endGame объекта ChessFrame - и соответствующая игра удаляется из хеш-таблицы. Если строка не соответствует ни одному из вышеописанных событий, она расценивается как стандартная команда сервера и попадает в окно выдачи информации:

public void receiveInput(InputStreamHandler ish, Object o) { String s = (String)o;

if (s.trim().equals("") || s.trim().equals("fics%")) { return;

}

if (s.trim().startsWith("<12>")) { parseBoard(s.trim());

} else if (s.trim().startsWith("{Game")) { StringTokenizer st = new StringTokenizer(s); String token = st.nextToken(); //{Game String number = st.nextToken(); //number

ChessFrame frame = (ChessFrame)games.get(number);

www.books-shop.com

if (frame!=null) {

token = st.nextToken(); //(player1 token = st.nextToken(); //vs. token = st.nextToken(); //player2) token = st.nextToken(); //loser frame.endGame(token); games.remove(number);

}

} else { output.appendText(s+"\n");

}

}

Данный метод вызывается, как только метод receiveInput обнаружит сообщение об обновлении игры. Шестнадцатым полем в строке является номер игры. Если данная игра отсутствует в нашей таблице, мы создаем новый объект ChessFrame и вносим его номер в таблицу. Наконец, мы обновляем окно при помощи метода updateGame:

protected void parseBoard(String s) { StringTokenizer st = new StringTokenizer(s); String token = st.nextToken();

if (! token.equals("<12>")) { output.appendText("oops... "+token);

return;

}

for (int i=0; i<<16; i++) { token = st.nextToken();

}

ChessFrame frame = (ChessFrame)games.get(token); if (frame==null) {

frame = new ChessFrame(this, user, fixed); games.put(token, frame);

}

frame.updateGame(s);

}

Все события, связанные с ходами игроков, обрабатываются здесь. Если местом назначения события является объект ChessFrame, событием должен быть ход игрока, сгенерированный доской, то есть его необходимо перенаправить серверу. Если местом назначения события является окно ввода, сообщение передается серверу, а окно ввода очищается:

public boolean action(Event evt, Object arg) { if (evt.target instanceof ChessFrame) {

writeOutput((String)arg+"\n"); return true;

}else if (evt.target==input) { writeOutput((String)arg+"\n"); input.setText("");

return true;

}else {

return false;

}

}

Шахматный клиент написан! Апплет выводит на экран свое название, окно выдачи информации с сервера и поле для ввода команд. Если пользователь начинает другую игру, создается новое окно ChessFrame, в котором можно играть, пользуясь удобным графическим интерфейсом. На рис. 18-2 воспроизведен фрагмент изображения работающего шахматного апплета.

www.books-shop.com

Рис. 18.2.

Возможные усовершенствования

Наш шахматный апплет был распространен среди широкой публики и, похоже, понравился многим. Как всегда, посыпались советы и рекомендации о том, как его можно усовершенствовать. Во-первых, процедура входа на сервер не маскирует вводимый пароль, следовательно, его может подсмотреть любой желающий. Во-вторых, апплет не устанавливает стиль доски 12 вместо 1, оставляя это на усмотрение игрока (см. рис. 18-1). В-третьих, некоторые игроки затруднялись отслеживать партнеров, готовых к сетевой игре, то есть им необходим отдельный список игроков в отдельном окне. Мы решили усовершенствовать апплет, выпустив его следующую версию.

Окно login

Окно login решено было оформить в виде отдельного, внешнего окна. Когда пользователь соединяется сервером, апплет наблюдает за окном login. Как только окно login сформировано, запускается процедура login. Кадр login состоит из двух полей текстового ввода - одно для имени, другое для пароля - и кнопки для запуска процедуры. В качестве окна login можно было бы применить стандартный диалог, поскольку он обладает свойством модальности. Это значит, что программа не реагирует ни на какое событие, не связанное с самим диалогом. К сожалению, на момент написания книги реализация диалога в Netscape Navigator 2.0 для Solaris была немного некорректной, поэтому нам пришлось обходиться без него. Вот исходный код класса

UserLogin:

import java.awt.*; import java.util.*;

public class UserLogin extends Frame { private Button login;

private TextField user; private TextField password; private Component target;

В конструкторе объекта UserLogin задается объект Component, которому следует посылать события, и устанавливается шрифт для отображения в окне login. Далее мы добавляем два текстовых поля TextField, одно для имени пользователя, другое - для его пароля. В последнем в качестве эхо-символа используется звездочка (*). Кроме того, мы добавляем кнопку login, генерирующую событие "login" и закрывающую окно:

public UserLogin(Component target, Font f) { super("Internet Chess Server Login");

www.books-shop.com

this.target = target; setBackground(Color.gray); setLayout(new FlowLayout()); Panel p = new Panel(); p.setLayout(new FlowLayout()); Label l = new Label("Username:"); p.add(l);

user = new TextField(16); user.setFont(f); user.setEditable(true); p.add(user);

l = new Label("Password:"); p.add(l);

password = new TextField(16); password.setFont(f); password.setEditable(true); password.setEchoCharacter('*'); p.add(password);

login = new Button("Login"); login.setFont(f); p.add(login);

add(p);

pack();

show();

}

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

public boolean action(Event evt, Object arg) { if (evt.target==login) {

if (user.getText().trim().equals("")) { return true;

}

Vector v = new Vector(2); v.addElement(user.getText()); v.addElement(password.getText());

Event e = new Event(this, Event.ACTION_EVENT, v); target.deliverEvent(e);

dispose(); return true;

}

return false;

}

Теперь добавим наше окно к основному апплету. Нам необходимо добавить код, обрабатывающий новое событие, генерируемое окном login. Окно UserLogin конструируется при запуске апплета, а исходный текст, добавляемый в конец метода start, выглядит следующим образом:

UserLogin ul = new UserLogin(this, pretty);

Кроме того, добавляется следующее условие:

} else if (evt.target instanceof UserLogin) { Vector v = (Vector)evt.arg;

user = (String)v.elementAt(0); String pass = (String)v.elementAt(1); writeOutput(user+"\n"+pass+"\n"); writeOutput("set style 12\n");

www.books-shop.com

return true;

}

Присваивать значение переменной user в этом методе безопаснее, чем в методе writeOutput. После того как присвоение переменной user из метода writeOutput удалено, можно считать, что усовершенствование состоялось. На рис. 18-3 изображено окно UserLogin.

Рис. 18.3.

Список текущих игроков

Добавление списка текущих игроков в отдельном окне - занятие, безусловно, более интересное, чем рассмотренное выше. Разработанное нами отдельное окно состоит из трех колонок. В нем отображаются имена игроков и количество набранных очков. Имя игрока - это кнопка, нажав на которую мы создаем новую игру с выбранным игроком. Одна из проблем отображения списка игроков связана с его динамичностью - список необходимо постоянно обновлять, следя за выбыванием старых и появлением новых игроков. Мы создаем отдельный поток, которой раз в минуту обновляет список. Для этого вводится класс Player, содержащий информацию об имени игрока и его блиц-рейтингом. Вот его исходный текст:

public class Player { public String name; public String blitz;

public Player(String name, String blitz) { this.name = name;

this.blitz = blitz;

}

}

Класс PlayerFrame создает кадр, в котором отображаются колонки с именами-кнопками и метками-очками. Входные данные передаются методом updatePlayerListing:

import java.awt.*; import java.util.*; import PackerLayout; import Player; import ChessClient;

public class PlayerFrame extends Frame { private ChessClient parent; private Vector playerList; private Panel topplayers; private Panel topscores;

www.books-shop.com