Когда в 30 томе “Библиотеки системного программиста” мы начинали разговор про язык программирования Java, то отмечали, что он специально ориентирован на глобальные сети, такие как Internet. В этой главе мы начнем знакомство с конкретными классами Java, разработанными для сетевого программирования. На примере наших приложений вы сможете убедиться, что классы Java действительно очень удобны для создания сетевых приложений.
В этой главе мы рассмотрим два аспекта сетевого программирования. Первый из них касается доступа из приложений Java к файлам, расположенным на сервере Web, второй - создания серверных и клиентских приложений с использованием сокетов.
Напомним, что из соображений безопасности алпетам полностью запрещен доступ к локальным файлам рабочей станции, подключенной к сети. Тем не менее, аплет может работать с файлами, расположенными на серверах Web. При этом можно использовать входные и выходные потоки, описанные нами в предыдущей главе.
Для чего аплетам обращаться к файлам сервера Web?
Таким аплетам можно найти множество применений.
Представьте себе, например, что вам нужно отображать у пользователя диаграмму, исходные данные для построения которой находятся на сервере Web. Эту задачу можно решить, грубо говоря, двумя способами.
Первый заключается в том, что вы создаете расширение сервера Web в виде приложения CGI или ISAPI, которое на основании исходных данных динамически формирует графическое изображение диаграммы в виде файла GIF и посылает его пользователю. Что касается расширения сервера Web, то вы сможете его создать, пользуясь 29 томом “Библиотеки системного программиста”, который называется “Сервер Web своими руками”.
Однако на пути решения задачи с помощью расширения сервера Web вас поджидают две неприятности. Во-первых, создать из программы красивый цветной графический файл в стандарте GIF не так-то просто - вы должны разобраться с форматом этого файла и создать все необходимые заголовки. Во-вторых, графический файл занимает много места и передается по каналам Internet достаточно медленно - средняя скорость передачи данных в Internet составляет 1 Кбайт в секунду.
В то же время файл с исходными данными может быть очень компактным. Возникает вопрос - нельзя ли передавать через Internet только исходные данные, а построение графической диаграммы выполнять на рабочей станции пользователя?
В этом заключается второй способ, который предполагает применение аплетов. Ниже мы приведем исходные тексты соотвестсвующего аплета в разделе “Приложение ShowChart”. Это приложение получает через сеть файл исходных данных, а затем на основании содержимого этого файла рисует в своем окне цветную круговую диаграмму. Объем передаваемых данных при этом по сравнению с использованием расширения сервера Web сокращается десятки раз.
Помимо работы с файлами, расположенными на сервере Web, в этой главе мы расскажем о создании каналов между приложениями Java, работающими на различных компьютерах в сети, с использованием сокетов.
Сокеты позволяют организовать тесное взаимодействие аплетов и полноценных приложений Java, при котором аплеты могут предавать друг другу данные через сеть Internet. Это открывает широкие возможности для обработки информации по схеме клиент-сервер, причем в роли серверов здесь может выступать любой компьютер, подключенный к сети, а не только сервер Web. Каждая рабочая станция может выступать одновременно и в роли сервера, и в роли клиента.
Прежде чем начинать создание сетевых приложений для Internet, вы должны разобраться с адресацией компьютеров в сети с протоколом TCP/IP, на базе которого построена сеть Internet. Подробную информацию об адресации вы можете получить из только что упомянутого 29 тома “Библиотеки системного программиста”. Здесь мы приведем только самые необходимые сведения.
Все компьютеры, подключенные к сети TCP/IP, называются узлами (в оригинальной терминологии узел - это host). Каждый узел имеет в сети свой адрес IP, состоящий из четырех десятичных цифр в диапазоне от 0 до 255, разделенных символом “точка “, например:
193.120.54.200
Фактически адрес IP является 32-разрядным двоичным числом. Упомянутые числа представляют собой отдельные байты адеса IP.
Так как работать с цифрами удобно лишь компьютеру, была придумана система доменных имен. При использовании этой системы адресам IP ставится в соответсвие так называемый доменный адрес, такой как www.microsoft.com.
В сети Internet имеется распределенная по всему миру база доменных имен, в которой установлено соответствие между доменными именами и адресами IP в виде четырех чисел.
Для работы с адресами IP в библиотеке классов Java имеется класс InetAddress, определение наиболее интересных методов которого приведено ниже:
public static InetAddress getLocalHost(); public static InetAddress getByName(String host); public static InetAddress[] getAllByName(String host); public byte[] getAddress(); public String toString(); public String getHostName(); public boolean equals(Object obj);
Рассмотрим применение этих методов.
Прежде всего вы должны создать объект класса InetAddress. Эта процедура выполняется не с помощью оператора new, а с применением статических методов getLocalHost, getByName и getAllByName.
Метод getLocalHost создает объект класса InetAddress для локального узла, то есть для той рабочей станции, на которой выполняется приложение Java. Так как этот метод статический, вы можете вызывать его, ссылаясь на имя класса InetAddress:
InetAddress iaLocal; iaLocal = InetAddress.getLocalHost();
В том случае, если вас интересует удаленный узел сети Internet или корпоративной сети Intranet, вы можете создать для него объект класса InetAddress с помощью методов getByName или getAllByName. Первый из них возвращает адрес узла, а второй - массив всех адресов IP, связанных с данным узлом. Если узел с указанным именем не существует, при выполнении методов getByName и getAllByName возникает исключение UnknownHostException.
Заметим, что методам getByName и getAllByName можно передавать не только имя узла, такое как “microsoft.com”, но и строку адреса IP в виде четырех десятичных чисел, разделенных точками.
После создания объекта класса InetAddress для локального или удаленного узла вы можете использовать другие методы этого класса.
Метод getAddress возвращает массив из чеырех байт адреса IP объекта. Байт с нулевым индексом этого массива содержит старший байт адреса IP.
Метод toString возвращает текстовую строку, которая содержит имя узла, разделитель ‘/’ и адрес IP в виде четырех десятичных чисел, разделенных точками.
С помощью метода getHostName вы можете определить имя узла, для которого был создан объект класса InetAddress.
И, наконец, метод equals предназначен для сравнения адресов IP как объектов класса InetAddress.
Приложение InetAddressDemo отображает имя и адрес IP локального узла, а затем запрашивает имя удаленного узла. Еси такой узел существует, для него определяется и отображается на консоли список адресов IP (рис. 3.1).
Рис. 3.1. Работа приложения InetAddressDemo
Если же указано имя несуществующего узла, возникает исключение UnknownHostException, о чем на консоль выводится сообщение.
Исходные тексты приложения InetAddressDemo приведены в листинге 3.1.
Листинг 3.1. Файл InetAddressDemo\InetAddressDemo.java
// ========================================================= // Работа с адресами IP с помощью класса InetAddress // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.net.*; import java.io.*; import java.util.*; // ========================================================= // Класс InetAddressDemo // Главный класс приложения // ========================================================= public class InetAddressDemo { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Введенная строка String sIn; // Рабочая строка String str; // Адрес локального узла InetAddress iaLocal; // Массив байт адреса локального узла byte[] iaLocalIP; // Массив всех адресов удаленного узла InetAddress[] iaRemoteAll; try { // Получаем адрес локального узла iaLocal = InetAddress.getLocalHost(); // Отображаем имя локального узла на консоли System.out.println("Local host name: " + iaLocal.getHostName()); // Определяем адрес IP локального узла iaLocalIP = iaLocal.getAddress(); // Отображаем отдельные байты адреса IP // локального узла System.out.println("Local host IP address: " + (0xff & (int)iaLocalIP[0]) + "." + (0xff & (int)iaLocalIP[1]) + "." + (0xff & (int)iaLocalIP[2]) + "." + (0xff & (int)iaLocalIP[3])); // Отображаем адрес IP локального узла, полученный // в виде текстовой строки System.out.println("Local host IP address: " + iaLocal.toString()); // Вводим имя удаленного узла, адрес которого // мы будет определять System.out.println("Enter remote host name..."); System.in.read(bKbdInput); sIn = new String(bKbdInput, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(sIn, "\r\n"); str = new String((String)st.nextElement()); // Получаем все адреса IP, свяжанные с удаленным // узлом, имя которого мы только что ввели iaRemoteAll = InetAddress.getAllByName(str); // Отображаем эти адреса на консоли for(int i = 0; i < iaRemoteAll.length; i++) { System.out.println("Remote host IP address: " + iaRemoteAll[i].toString()); } System.out.println("Press <Enter> to terminate..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Сразу после запуска приложение создает кобъект класса InetAddress для локального узла, вызывая для этого статический метод getLocalHost:
iaLocal = InetAddress.getLocalHost();
Далее для созданного объекта вызывается метод getHostName, возвращающий строку имени локального узла:
System.out.println("Local host name: " + iaLocal.getHostName());
Это имя отображается на консоли приложения.
Затем приложение определяет адрес IP локального узла, вызывая метод getAddress:
iaLocalIP = iaLocal.getAddress();
Напомним, что этот метод возвращает массив четырех байт адреса IP.
Адрес IP мы отображаем на консоли с помощью метода println:
System.out.println("Local host IP address: " + (0xff & (int)iaLocalIP[0]) + "." + (0xff & (int)iaLocalIP[1]) + "." + (0xff & (int)iaLocalIP[2]) + "." + (0xff & (int)iaLocalIP[3]));
Заметьте, что байты адреса записваются в массив типа byte как знаковые величины. Для того чтобы отображить их на консоли в виде положительных чисел, мы вначале выполняем явное преобразование к типу int, а затем обнуляем старший байт (так как такое преобразование выполняется с сохранением знака).
Наше приложение демонстрирует также другой способ получения адреса IP для объекта класса InetAddress, который заключается в вызове метода toString:
System.out.println("Local host IP address: " + iaLocal.toString());
На втором этапе приложение InetAddressDemo вводит строку имени удаленного узла и, после удаления символа перехода на новую строку, пытается создать для введенного имени массив объектов класса InetAddress. Для этого приложение вызывает метод getAllByName:
iaRemoteAll = InetAddress.getAllByName(str);
Содержимое созданного массива отображается в цикле, причем адрес IP извлекается из объектов класса InetAddress методом toString:
for(int i = 0; i < iaRemoteAll.length; i++) { System.out.println("Remote host IP address: " + iaRemoteAll[i].toString()); }
Адрес IP позволяет идентифицировать узел, однако его недостаточно для идентификации ресурсов, имеющихся на этом узле, таких как работающие приложения или файлы. Причина очевидна - на узле, имеющем один адрес IP, может существовать много различных ресурсов.
Для ссылки на ресурсы сети Internet применяется так называемый универсальный адрес ресуросв URL (Universal Resource Locator). В общем виде этот адрес выглядит следующим образом:
[protocol]://host[:port][path]
Строка адреса начинаетс с протокола protocol, который должен быть использован для доступа к ресурсу. Документы HTML, например, передаются из сервера Web удаленным пользователям с помощью протокола HTTP. Файловые серверы в сети Internet работают с протоколом FTP.
Для ссылки на сетевые ресурсы через протокол HTTP используется следующая форма универсального адреса ресурсов URL:
http://host[:port][path]
Параметр host обязательный. Он должен быть указан как доменный адрес или как адрес IP (в виде четырех десятичных чисел). Например:
http://www.microsoft.com http://154.23.12.101
Необязательный параметр port задает номер порта для работы с сервером. По умолчанию для протокола HTTP используется порт с номером 80, однако для специализированных серверов Web это может быть и не так.
Номер порта идентифицирует программу, работающую в узле сети TCP/IP и взаимодействующую с другими программами, расположенными на том же или на другом узле сети. Если вы разрабатываете программу, передающую данные через сеть TCP/IP с использованием, например, интерфейса сокетов, то при создании канала связи с уделенным компьютером вы должны указать не только адрес IP, но и номер порта, который будет использован для передачи данных.
Ниже мы показали, как нужно указывать в адресе URL номер порта:
http://www.myspecial.srv/:82
Теперь займемся параметром path, определяющем путь к объекту.
Обычно любой сервер Web или FTP имеет корневой каталог, в котором расположены подкаталоги. Как в корневом каталоге, так и в подкаталогах сервера Web могут находиться документы HTML, двоичные файлы, файлы с графическими изображениями, звуковые и видео-файлы, расширения сервера в виде программ CGI или библиотек динамической компоновки, дополняющих возможности сервера (такие, как библиотеки ISAPI для сервера Microsoft Information Server).
Если в качестве адреса URL указать навигатору только доменное имя сервера, сервер перешлет навигатору свою главную страницу. Имя файла этой страницы зависит от сервера. Большинство серверов на базе операционной системы UNIX посылают по умолчанию файл документа с именем index.html. Сервер Microsoft Information Server может использовать для этой цели имя default.htm или любое другое, определенное при установке сервера, например, home.html или home.htm.
Для ссылки на конкретный документ HTML или на файл любого другого объекта необходимо указать в адресе URL его путь, включающий имя файла, например:
http://www.glasnet.ru/~frolov/index.html http://www.dials.ccas.ru/frolov/bin/dbsp26.lzh
Корневой каталог сервера Web обозначается символом /. В спецификации протокола HTTP сказано, что если путь не задан, то используется корневой каталог.
Для работы с ресурсами, заданными своими адресами URL, в библиотеке классов Java имеется очень удобный и мощный класс с названием URL. Простота создания сетевых приложений с использованием этого класса в значительной степени опровергает общераспространенное убеждение в сложности сетевого программирования. Инкапсулируя в себе достаточно сложные процедуры, класс URL предоставляет в распоряжение программиста небольшой набор простых в использовании конструкторов и методов.
Сначала о конструкторах. Их в классе URL имеется четыре штуки.
public URL(String spec);
Первый из них создает объект URL для сетевого ресурса, адрес URL которого передается конструктору в виде текстовой строки через единственный параметр spec:
public URL(String spec);
В процессе создания объекта проверяется заданный адрес URL, а также наличие указанного в нем ресурса. Если адрес указан неверно или заданный в нем ресурс отсутствует, возникает исключение MalformedURLException. Это же исключение возникает при попытке использовать протокол, с которым данная система не может работать.
Второй вариант конструктора класса URL допускает раздельное указание протокола, адреса узла, номера порта, а также имя файла:
public URL(String protocol, String host, int port, String file);
Третий вариант предполагает использование номера порта, принятого по умолчанию:
public URL(String protocol, String host, String file);
Для протокола HTTP это порт с номером 80.
И, наконец, четвертый вариант конструктора допускает указание контекста адреса URL и строки адреса URL:
public URL(URL context, String spec);
Строка контекста позволяет указывать компоненты адреса URL, отсустсвующие в строке spec, такие как протокол, имя узла, файла или номер порта.
Рассмотрим самые интересные методы, определенные в классе URL.
Метод openStream позволяет создать входной поток для чтения файла ресурса, связанного с созданным объектом класса URL:
public final InputStream openStream();
Для выполнения операции чтения из созданного таким образом потока вы можете использовать метод read, определенный в классе InputStream (любую из его разновидностей).
Данную пару методов (openStream из класса URL и read из класса InputStream) можно применить для решения задачи получения содержимого двоичного или текстового файла, хранящегося в одном из каталогов сервера Web. Сделав это, обычное приложение Java или аплет может выполнить локальную обработку полученного файла на компьютере удаленного пользователя.
Очень интересен метод getConten. Этот метод определяет и получает содержимое сетевого ресурса, для которого создан объект URL:
public final Object getContent();
Практически вы можете использовать метод getContent для получения текстовых файлов, расположенных в сетевых каталогах.
К сожалению, данный метод непригоден для получения документов HTML, так как для данного ресурса не определен обработчик соедржимого, предназначенный для создания объекта. Метод getContent не способен создать объект ни из чего другого, кроме текстового файла.
Данная проблема, тем не менее, решается очень просто - достаточно вместо метода getContent использовать описанную выше комбинацию методов openStream из класса URL и read из класса InputStream.
С помощью метода getHost вы можете определить имя узла, соответствующего данному объекту URL:
public String getHost();
Метод getFile позволяет получить информацию о файле, связанном с данным объектом URL:
public String getFile();
Метод getPort предназначен для определения номера порта, на котором выполняется связь для объекта URL:
public int getPort();
С помощью метода getProtocol вы можете определить протокол, с использованием которого установлено соединение с ресурсом, заданным объектом URL:
public String getProtocol();
Метод getRef возвращает текстовую строку ссылки на ресурс, соответствующий данному объекту URL:
public String getRef();
Метод hashCode возвращает хэш-код объекта URL:
public int hashCode();
С помощью метода sameFile вы можете определить, ссылаются ли два объекта класса URL на один и тот же ресурс, или нет:
public boolean sameFile(URL other);
Если объекты ссылаются на один и тот же ресурс, метод sameFile возвращает значение true, если нет - false.
Вы можете использовать метод equals для определения идентичности адресов URL, заданных двумя объектами класса URL:
public boolean equals(Object obj);
Если адреса URL идентичны, метод equals возвращает значение true, если нет - значение false.
Метод toExternalForm возвращает текстовую строку внешнего представления адреса URL, определенного данным объектом класса URL:
public String toExternalForm();
Метод toString возвращает текстовую строку, представляющую данный объект класса URL:
public String toString();
Метод openConnection предназначен для создания канала между приложением и сетевым ресурсом, представленным объектом класса URL:
public URLConnection openConnection();
Если вы создаете приложение, которое позволяет читать из каталогов сервера Web текстовые или двоичные файлы, можно создать поток методом openStream или получить содержимое текстового ресурса методом getContent.
Однако есть и другая возможность. Вначале вы можете создать канал, как объект класса URLConnection, вызвав метод openConnection, а затем создать для этого канала входной поток, воспользовавшись методом getInputStream, определенным в классе URLConnection. Такая методика позволяет определить или установить перед созданием потока некоторые характеристики канала, например, задать кэширование.
Однако самая интересная возможность, которую предоставляет этот метод, заключается в организации взаимодействия приложения Java и сервера Web.
Подробнее методика организации такого взаимодействия и класс URLConnection будет рассмотрен позже.
В качестве практического примера применения класса URL мы создали приложение URLDemo. Это приложение вводит с консоли адрес URL текстового или двоичного файла, расположенного на сервере Web и создает для этого файла входной поток. С использованием данного потока приложение копирует файл на локальный диск компьютера в текущий каталог.
Исходный текст приложения URLDemo приведены в листинге 3.2.
Листинг 3.2. Файл URLDemo\URLDemo.java
// ========================================================= // Копирование файла, расположенного в каталоге // сервера Web, с помощью класса URL // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.net.*; import java.io.*; import java.util.*; // ========================================================= // Класс InetAddressDemo // Главный класс приложения // ========================================================= public class URLDemo { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Введенная строка String sIn; // Строка адреса URL String sURL; // Адрес URL удаленного узла URL u; // Рабочий буфер для копирования файла byte buf[] = new byte[1024]; try { // Вводим адрес URL удаленного узла System.out.println("Enter remote host name..."); System.in.read(bKbdInput); sIn = new String(bKbdInput, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(sIn, "\r\n"); sURL = new String((String)st.nextElement()); // Создаем объект класса URL u = new URL(sURL); // Создаем входной поток, связанный с объектом, // адрес URL которого хранится в поле u InputStream is = u.openStream(); // Создаем выходной буферизованный форматированный // поток для записи принятого файла DataOutputStream os = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("output.dat"))); // Выполняем в цикле чтение файла, расположенного // по адресу u, копируя этот файл в выходной поток while(true) { int nReaded = is.read(buf); if(nReaded == -1) break; os.write(buf, 0, nReaded); } // Закрываем входной и выходной потоки is.close(); os.close(); System.out.println("File received"); } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println("Press <Enter> to terminate..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Сразу после запуска приложение запрашивает с консоли текстовую строку адреса URL файла, который необходимо переписать через сеть на локальный диск. После удаления символа перевода строки адрес записывается в поле sURL.
Далее приложение создает объект класса URL, соответствующий введенному адресу:
u = new URL(sURL);
На следующем этапе для объекта URL создается входной поток, для чего вызывается метод openStream:
InputStream is = u.openStream();
Идентификатор этого потока сохраняется в поле is.
Принятый файл будет записан в текущий каталог под именем output.dat. Для этого мы создаем входной буферизованный форматированный поток os, как это показано ниже:
DataOutputStream os = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("output.dat")));
После знакомства с главой нашей книги, посвященной работе с потоками, эти строки не должны вызывать у вас никаких вопросов.
Операция чтения данных из входного потока и записи в выходной поток выполняется в цикле:
while(true) { int nReaded = is.read(buf); if(nReaded == -1) break; os.write(buf, 0, nReaded); }
Вначале для входного потока вызывается метод read. Он возвращает количество прочитанных байт данных или значение -1, если был достигнут конец потока. В последнем случае цикл прерывается.
Принятые данные размещаются в массиве buf, откуда затем они записываются в выходной поток методом write. Мы записываем в выходной поток столько байт данных, сколько было считано.
После того как файл будет принят и записан в выходной поток, мы закрываем оба потока:
is.close(); os.close();
Попробуем теперь на практике применить технологию передачи файлов из каталога сервера Web в аплет для локальной обработки. Наше следующее приложение с названием ShowChart получает небольшой текстовый файл с исходными данными для построения круговой диаграммы, содержимое которого представлено ниже:
10,20,5,35,11,10,3,6,80,10,20,5,35,11,10,3,6,80
В этом файле находятся численные значения углов для отдельных секторов диаграммы, причем сумма этих значений равна 360 градусам. Наш аплет принимает этот файл через сеть и рисует круговую диаграмму, показанную на рис. 3.2.
Рис. 3.2. Круговая диаграмма, построенная на базе исходных данных, полученных через сеть
Файл исходных данных занимает всего 49 байт, поэтому он передается по сети очень быстро. Если бы мы передавали графическое изображение этой диаграммы, статическое или динамическое, подготовленное, например, расширением сервера CGI или ISAPI, объем передаваемых по сети данных был бы намного больше.
Исходные тексты приложения ShowChart приведены в листинге 3.3.
Листинг 3.3. Файл ShowChart\ShowChart.java
// ========================================================= // Рисование круговой диаграммы, данные для которой // получены от сервера Web через сеть // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.applet.*; import java.awt.*; import java.net.*; import java.io.*; import java.util.*; public class ShowChart extends Applet { // Адрес URL файла с данными для круговой диаграммы URL SrcURL; // Содержимое этого файла Object URLContent; // Код ошибки int errno = 0; // ------------------------------------------------------- // getAppletInfo // Метод, возвращающей строку информации об аплете // ------------------------------------------------------- public String getAppletInfo() { return "Name: ShowChart\r\n" + "Author: Alexandr Frolov\r\n" + "E-mail: frolov@glas.apc.org" + "WWW: http://www.glasnet.ru/~frolov" + "Created with Microsoft Visual J++ Version 1.0"; } // ------------------------------------------------------- // init // Метод, получающий управление при инициализации аплета // ------------------------------------------------------- public void init() { try { // Создаем объект класса URL для файла с данными // для круговой диаграммы SrcURL = new URL("http://frolov/chart.txt"); try { // Получаем содержимое этого файла URLContent = SrcURL.openConnection().getContent(); } catch (IOException ioe) { showStatus("getContent exception"); // При возникновении исключения во время получения // содержимого устанавливаем код ошибки, равный 1 errno = 1; } } catch (MalformedURLException uex) { showStatus("MalformedURLException exception"); // При возникновении ошибки в процессе создания // объекта класса URL устанавливаем код ошибки, // равный 2 errno = 2; } } // ------------------------------------------------------- // paint // Метод paint, выполняющий рисование в окне аплета // ------------------------------------------------------- public void paint(Graphics g) { // Строка, в которую будет записано содержимое // файла данных для круговой диаграммы String sChart = "<error>"; // Начальный угол сектора диаграммы Integer AngleFromChart = new Integer(0); // Угол предыдущего сектора диаграммы int PrevAngle = 0; // Случайные компоненты цвета сектора int rColor, gColor, bColor; // Определяем текущие размеры окна аплета Dimension dimAppWndDimension = size(); // Выбираем в контекст отображения желтый цвет g.setColor(Color.yellow); // Закрашиваем внутреннюю область окна аплета g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Выбираем в контекст отображения черный цвет g.setColor(Color.black); // Рисуем рамку вокруг окна аплета g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Проверяем, является ли полученное содержимое // текстовой строкой if(URLContent instanceof String) { sChart = (String)URLContent; } // Если нет, устанавливаем код ошибки, равный 3 else errno = 3; // Если произошла ошибка, отображаем ее код // и полученные данные if(errno != 0) showStatus("errno: " + errno + ", sChart: " + sChart); // Если ошибки нет, отображаем полученные данные else showStatus(sChart); // Создаем разборщик текстовой строки для // выделения значений углов в принятом файле данных StringTokenizer st = new StringTokenizer(sChart, ",\r\n"); // Цикл по всем значениям while(st.hasMoreElements()) { // Выбираем случайный цвет для рисования rColor = (int)(255 * Math.random()); gColor = (int)(255 * Math.random()); bColor = (int)(255 * Math.random()); // Устанавливаем выбранный цвет в контексте // отображения g.setColor(new Color(rColor, gColor, bColor)); // Получаем значение угла String angle = (String)st.nextElement(); // Преобразуем его в численное значение AngleFromChart = new Integer(angle) ; // Рисуем сектор диаграммы g.fillArc(0, 0, 200, 200, PrevAngle, AngleFromChart.intValue()); // Увеличиваем текущее значение угла PrevAngle += AngleFromChart.intValue(); } } }
Исходный текст документа HTML, созданного автоматически для нашего аплета, представлен в листинге 3.4.
Листинг 3.4. Файл ShowChart\ShowChart.html
<html> <head> <title>ShowChart</title> </head> <body> <hr> <applet code=ShowChart.class id=ShowChart width=200 height=200 > </applet> <hr> <a href="ShowChart.java">The source.</a> </body> </html>
Приложение ShowChart получает содержимое файла исходных данных для построения круговой диаграммы с помощью класса URL. Как вы увидите, для получения содержимого этого файла оно не создает поток ввода явным образом, как это делало предыдущее приложение (хотя могло бы). Вместо этого оно пользуется методом getContent, определенным в классе URL.
В классе ShowChart определены три поля.
Поле SrcURL класса URL хранит адрес URL файла исходных данных для круговой диаграммы. В поле URLContent типа Object будет переписано содержимое этого файла. И, наконец, в поле errno хранится текущий код ошибки, если она возникла, или нулевое значение, если все операции были выполнены без ошибок.
Во время инициализации метод init создает объект класса URL для файла исходных данных:
SrcURL = new URL("http://frolov/chart.txt");
Здесь для экономии места в книге мы указали адрес URL файла исходных данных непосредственно в программе, однако вы можете передать этот адрес аплету через параметр в документе HTML.
Далее для нашего объекта URL мы создаем канал и получаем содержимое объекта (то есть исходные данные для построения диаграммы):
URLContent = SrcURL.openConnection().getContent();
Здесь использована двухступенчатая процедура получения содержимого с созданием канала как объекта класса URLConnection. Вы также можете упростить этот код, воспользовавшись методом getContent из класса URL:
URLContent = SrcURL.getContent();
Результат в обоих случаях будет одинаковый - содержимое файла исходных данных окажется записанным в поле URLContent класса Object.
Если при создании объекта класса URL возникло исключение, метод init записывает в поле errno код ошибки, равный 2, записывая при этом в строку состояния навигатора сообщение “MalformedURLException exception”.
В том случае, когда объект класса URL создан успешно, а исключение возникло в процессе получения содержимого файла, в поле errno записывается значение 1, а в строку состояния навигатора - сообщение "getContent exception".
После раскрашивания фона окна аплета и рисования вокруг него рамки метод paint приступает к рисованию круговой диаграммы.
Прежде всего метод проверяет, является ли полученный из сети объект текстовой строкой класса String. Если является, то выполняется явное преобразование типа:
if(URLContent instanceof String) { sChart = (String)URLContent; }
В случае успеха в переменной sChart будет находиться строка исходных данных для построения диаграммы, а при ошибке - строка “<error>”, записанная туда при инициализации. Кроме того, в поле errno записывается значение 3.
Далее метод paint проверяет, были ли ошибки при создании объекта URL, получении содержимого файла исходных данных или преобразования данных в строку класса String. Если были, то в строку состояния навигатора записывается код ошибки и содержимое строки sChart. Если же ошибок не было, то в строке состояния отображаются исходные данные:
if(errno != 0) showStatus("errno: " + errno + ", sChart: " + sChart); else showStatus(sChart);
На следующем этапе обработчик paint приступает к построению диаграммы.
Первым делом создается разборщик строки исходных данных:
StringTokenizer st = new StringTokenizer(sChart, ",\r\n");
В качестве разделителей для этого разборщика указывается запятая, символ возврата каретки и перевода строки.
Рисование секторов диаграммы выполняется в цикле, условием выхода из которого является завершение разбора строки исходных данных:
while(st.hasMoreElements()) { . . . }
Для того чтобы секторы диаграммы не сливались, они должны иметь разный цвет. Цвет сектора можно было бы передавать вместе со значением угла через файл исходных данных, однако мы применили более простой способ раскаршивания секторов - в случайные цвета. Мы получаем случайные компоненты цвета сектора, а затем выбираем цвет в контекст отображения:
rColor = (int)(255 * Math.random()); gColor = (int)(255 * Math.random()); bColor = (int)(255 * Math.random()); g.setColor(new Color(rColor, gColor, bColor));
С помощью метода nextElement мы получаем очередное значение угла сектора и сохраняем его в переменной angle:
String angle = (String)st.nextElement();
Далее с помощью конструктора класса Integer это значение преобразуется в численное:
AngleFromChart = new Integer(angle) ;
Рисование сектора круговой диаграммы выполняется с помощью метода fillArc, который был рассмотрен в предыдущем томе “Библиотеки системного программиста”, посвященном языку программирования Java:
g.fillArc(0, 0, 200, 200, PrevAngle, AngleFromChart.intValue());
В качестве начального значения угла сектора используется значение из переменной PrevAngle. Сразу после инициализации в эту переменную записывается нулевое значение.
Конечный угол сектора задается как AngleFromChart.intValue(), то есть указывается значение, полученное из принятого по сети файла исходных данных.
После завершения рисования очередного сектора круговой диаграммы начальное значение PrevAngle увеличивается на величину угла нарисованного сектора:
PrevAngle += AngleFromChart.intValue();
В библиотеке классов Java есть очень удобное средство, с помощью которых можно организовать взаимодействие между приложениями Java и аплетами, работающими как на одном и том же, так и на разных узлах сети TCP/IP. Это средство, родившееся в мире операционной системы UNIX, - так называемые сокеты (sockets).
Что такое сокеты?
Вы можете представить себе сокеты в виде двух розеток, в которые включен кабель, предназначенный для передачи данных через сеть. Переходя к компьютерной терминологии, скажем, что сокеты - это программный интерфейс, предназначенный для передачи данных между приложениями.
Прежде чем приложение сможет выполнять передачу аили прием данных, оно должно создать сокет, указав при этом адрес узла IP, номер порта, через который будут передаваться данные, и тип сокета.
С адресом узла IP вы уже сталкивались. Номер порта служит для идентификации приложения. Заметим, что существуют так называемые “хорошо известные” (well known) номера портов, зарезервированные для различных приложений. Например, порт с номером 80 зарезервирован для использования серверами Web при обмене данными через протокол HTTP.
Что же касается типов сокетов, то их два - потоковые и датаграммные.
С помощью потоковых сокетов вы можете создавать каналы передачи данных между двумя приложениями Java в виде потоков, которые мы уже рассматривали во второй главе. Потоки могут быть входными или выходными, обычными или форматированными, с использованием или без использования буферизации. Скоро вы убедитесь, что организовать обмен данными между приложениями Java с использованием потоковых сокетов не труднее, чем работать через потоки с обычными файлами.
Заметим, что потоковые сокеты позволяют передавать данные только между двумя приложениями, так как они предполагают создание канала между этими приложениями. Однако иногда нужно обеспечить взаимодействие нескольких клиентских приложений с одним серверным. В этом случае вы можете либо создавать в серверном приложении отдельные задачи и отдельные каналы для каждого клиентского приложения, либо воспользоваться датаграммными сокетами. Последние позволяют передавать данные сразу всем узлам сети, хотя такая возможность редко используется и часто блокируется администраторами сети.
Для передачи данных через датаграммные сокеты вам не нужно создавать канал - данные посылаются непосредственно тому приложению, для которого они предназначены с использованием адреса этого приложения в виде сокета и номера порта. При этом одно клиентское приложение может обмениваться данными с несколькими серверными приложениями или наоборот, одно серверное приложение - с несколькими клиентскими.
К сожалению, датаграммные сокеты не гарантируют доставку передаваемых пакетов данных. Даже если пакеты данных, передаваемые через такие сокеты, дошли до адресата, не гарантируется, что они будут получены в той же самой последовательности, в которой были переданы. Потоковые сокеты, напротив, гарантируют доставку пакетов данных, причем в правильной последовательности.
Причина отстутствия гарантии доставки данных при использовании датаграммных сокетов заключается в использовании такими сокетами протокола UDP, который, в свою очередь, основан на протоколе с негарантированной доставкой IP. Потоковые сокеты работают через протокол гарантированной доставки TCP.
В 23 томе “Библиотеки системного программиста”, который называется “Глобальные сети компьютеров. Практическое введение в Internet, E-Mail, FTP, WWW и HTML, программирование для Windows Sockets” мы уже рассказывали про сокеты в среде операционной системы Microsoft Windows. В этой книге вы найдете примеры приложений, составленных на языке программирования С и работающих как с потоковыми, так и с датаграммными сокетами.
Как мы уже говорили, интерфейс сокетов позволяет передавать данные между двумя приложениями, работающими на одном или разных узлах сети. В процессе создания канала передачи данных одно из этих приложений выполняет роль сервера, а другое - роль клиента. После того как канал будет создан, приложения становятся равноправными - они могут передавать друг другу данные симметричным образом.
Рассмотрим этот процесс в деталях.
Вначале мы рассмотрим действия приложения, которое на момент инициализации является сервером.
Первое, что должно сделать серверное приложение, это создать объект класса ServerSocket, указав конструктору этого класса номер используемого порта:
ServerSocket ss; ss = new ServerSocket(9999);
Заметим, что объект класса ServerSocket вовсе не является сокетом. Он предназначен всего лишь для установки канала связи с клиентским приложением, после чего создается сокет класса Socket, пригодный для передачи данных.
Установка канала связи с клиентским приложением выполняется при помощи метода accept, определенного в классе ServerSocket:
Socket s; s = ss.accept();
Метод accept приостанавливает работу вызвавшей его задачи до тех пор, пока клиентское приложение не установит канал связи с сервером. Если ваше приложение однозадачное, его работа будет блокирована до момента установки канала связи. Избежать полной блокировки приложения можно, если выполнять создание канала передачи данных в отдельной задаче.
Как только канал будет создан, вы можете использовать сокет сервера для образования входного и выходного потока класса InputStream и OutputStream, соответственно:
InputStream is; OutputStream os; is = s.getInputStream(); os = s.getOutputStream();
Эти потоки можно использовать таким же образом, что и потоки, связанные с файлами.
Обратите также внимание на то, что при создании серверного сокета мы не указали адрес IP и тип сокета, ограничившись только номером порта.
Что касается адреса IP, то он, очевидно, равен адресу IP узла, на котором запущено приложение сервера. В классе ServerSocket определен метод getInetAddress, позволяющий определить этот адрес:
public InetAddress getInetAddress();
Тип сокета указывать не нужно, так как для работы с датаграммными сокетами предназначен класс DatagramSocket, который мы рассмотрим позже.
Процесс инициализации клиентского приложения выглядит весьма просто. Клиент должен просто создать сокет как объект класса Socket, указав адрес IP серверного приложения и номер порта, используемого сервером:
Socket s; s = new Socket("localhost",9999);
Здесь в качестве адреса IP мы указали специальный адрес localhost, предназначенный для тестирования сетевых приложений, а в качестве номера порта - ззначение 9999, использованное сервером.
Теперь можно создавать входной и выходной потоки. На стороне клиента эта операция выполняется точно также, как и на стороне сервера:
InputStream is; OutputStream os; is = s.getInputStream(); os = s.getOutputStream();
После того как серверное и клиентское приложения создали потоки для приема и передачи данных, оба этих приложения могут читать и писать в канал данных, вызывая методы read и write, определенные в классах InputStream и OutputStream.
Ниже мы представили фрагмент кода, в котором приложение вначале читает данные из входного потока в буфер buf, а затем записывает прочитанные данные в выходной поток:
byte buf[] = new byte[512]; int lenght; lenght = is.read(buf); os.write(buf, 0, lenght); os.flush();
На базе потоков класса InputStream и OutputStream вы можете создать буферизованные потоки и потоки для передачи форматированных данных, о которых мы рассказывали раньше.
После завершения передачи данных вы должны закрыть потоки, вызвав метод close:
is.close(); os.close();
Когда канал передачи данных больше не нужен, сервер и клиент должны закрыть сокет, вызвав метод close, определенный в классе Socket:
s.close();
Серверное приложение, кроме того, должно закрыть соединение, вызвав метод close для объекта класса ServerSocket:
ss.close();
После краткого введения в сокеты приведем описание наиболее интересных конструкторов и методов класса Socket.
Чаще всего для создания сокетов в клиентских приложениях вы будете использовать один из двух конструкторов, прототипы которых приведены ниже:
public Socket(String host, int port); public Socket(InetAddress address, int port);
Первый из этих конструкторов позволяет указывать адрес серверного узла в виде текстовой строки, второй - в виде ссылки на объект класса InetAddress. Вторым параметром задается номер порта, с использованием которого будут передаваться данные.
В классе Socket определена еще одна пара конструкторов, которая, однако не рекомендуется для использования:
public Socket(String host, int port, boolean stream); public Socket(InetAddress address, int port, boolean stream);
В этих конструкторах последний параметр определяет тип сокета. Если этот параметр равен true, создается потоковый сокет, а если false - датаграммный. Заметим, однако, что для работы с датаграммными сокетами следует использовать класс DatagramSocket.
Перечислим наиболее интересные, на наш взгляд, методы класса Socket.
Прежде всего, это методы getInputStream и getOutputStream, предназначенные для создания входного и выходного потока, соответственно:
public InputStream getInputStream(); public OutputStream getOutputStream();
Эти потоки связаны с сокетом и должны быть использованы для передачи данных по каналу связи.
Методы getInetAddress и getPort позволяют определить адрес IP и номер порта, связанные с данным сокетом (для удаленного узла):
public InetAddress getInetAddress(); public int getPort();
Метод getLocalPort возвращает для данного сокета номер локального порта:
public int getLocalPort();
После того как работа с сокетом завершена, его необходимо закрыть методом close:
public void close();
И, наконец, метод toString возвращает текстовую строку, представляющую сокет:
public String toString();
В качестве примера мы приведем исходные тексты двух приложений Java, работающих с потоковыми сокетами. Одно из этих приложений называется SocketServ и выполняет роль сервера, второе называется SocketClient и служит клиентом.
Приложение SocketServ выводит на консоль строку “Socket Server Application” и затем переходит в состояние ожидания соединения с клиентским приложением SocketClient.
Приложение SocketClient устанавливает соединение с сервером SocketServ, используя потоковый сокет с номером 9999 (этот номер выбран нами произвольно). Далее клиентское приложение выводит на свою консоль приглашение для ввода строк. Введенные строки отображаются на консоли и передаются серверному приложению. Сервер, получив строку, отображает ее в своем окне и посылает обратно клиенту. Клиент выводит полученную от сервера строку на консоли.
Когда пользователь вводит строку “quit”, цикл ввода и передачи строк завершается.
Весь процесс показан на рис. 3.3.
Рис. 3.3. Передача данных между приложениями SocketClient и SocketServ через потоковый сокет
Здесь в окне клиентского приложения мы ввели несколько строк, причем последняя строка была строкой “quit”, завершившая работу приложений.
Исходный текст серверного приложения SocketServ приведен в листинге 3.5.
Листинг 3.5. Файл SocketServ\SocketServ.java
// ========================================================= // Использование потоковых сокетов. // Приложение сервера // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.io.*; import java.net.*; import java.util.*; public class SocketServ { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Объект класса ServerSocket для создания канала ServerSocket ss; // Сокет сервера Socket s; // Входной поток для приема команд от клиента InputStream is; // Выходной поток для передачи ответа клиенту OutputStream os; try { System.out.println("Socket Server Application"); } catch(Exception ioe) { // При возникновении исключения выводим его описание // на консоль System.out.println(ioe.toString()); } try { // Создаем объект класса ServerSocket ss = new ServerSocket(9999); // Ожидаем соединение s = ss.accept(); // Открываем входной поток для приема // команд от клиента is = s.getInputStream(); // Открываем выходной поток для передачи // ответа клиенту os = s.getOutputStream(); // Буфер для чтения команд byte buf[] = new byte[512]; // Размер принятого блока данных int lenght; // Цикл обработки команд, полученных от клиента while(true) { // Получаем команду lenght = is.read(buf); // Если входной поток исчерпан, завершаем // цикл обработки команд if(lenght == -1) break; // Отображаем принятую команду на консоли сервера // Формируем строку из принятого блока String str = new String(buf, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(str, "\r\n"); str = new String((String)st.nextElement()); // Выводим строку команды на консоль System.out.println("> " + str); // Посылаем принятую команду обратно клиенту os.write(buf, 0, lenght); // Сбрасываем буфер выходного потока os.flush(); } // Закрываем входной и выходной потоки is.close(); os.close(); // Закрываем сокет сервера s.close(); // Закрываем соединение ss.close(); } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println( "Press <Enter> to terminate application..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
В методе main, получающем управление сразу после запуска приложения, мы определили несколько переменных.
Массив bKbdInput размером 256 байт предназначен для хранения строк, введенных при помощи клавиатуры.
В переменную ss класса ServerSocket будет записана ссылка на объект, предназначенный для установления канала связи через потоковый сокет (но не ссылка на сам сокет):
ServerSocket ss;
Ссылка на сокет, с использованием которого будет происходить передача данных, хранится в переменной с именем s класса Socket:
Socket s;
Кроме того, мы определили переменные is и os, соответственно, классов InputStream и OutputStream:
InputStream is; OutputStream os;
В эти переменные будут записаны ссылки на входной и выходной поток данных, которые связаны с сокетом.
После отображения на консоли строки названия приложения, метод main создает объект класса ServerSocket, указывая конструктору номер порта 9999:
ss = new ServerSocket(9999);
Конструктор возвращает ссылку на объект, с использованием которого можно установить канал передачи данных с клиентом.
Канал устанавливается методом accept:
s = ss.accept();
Этот метод переводит приложение в состояние ожидания до тех пор, пока не будет установлен канал передачи данных.
Метод accept в случае успешного создания канала передачи данных возвращает ссылку на сокет, с применением которого нужно принимать и передавать данные.
На следующем этапе сервер создает входной и выходной потоки, вызывая для этого методы getInputStream и getOutputStream, соответственно:
is = s.getInputStream(); os = s.getOutputStream();
Далее приложение подготавливает буфер buf для приема данных и определяет переменную length, в которую будет записываться размер принятого блока данных:
byte buf[] = new byte[512]; int lenght;
Теперь все готово для запуска цикла приема и обработки строк от клиентского приложения.
Для чтения строки мы вызываем метод read применительно ко входному потоку:
lenght = is.read(buf);
Этот метод возвращает управление только после того, как все данные будут прочитаны, блокируя приложение на время своей работы. Если такая блокировка нежелательна, вам следует выполнять обмен данными через сокет в отдельной задаче.
Метод read возвращает размер принятого блока данных или -1, если поток исчерпан. Мы воспользовались этим обстоятельством для завершения цикла приема данных:
if(lenght == -1) break;
После завершения приема блока данных мы преобразуем массив в текстовую строку str класса String, удаляя из нее символ перевода строки, и отображаем результат на консоли сервера:
System.out.println("> " + str);
Затем полученная строка отправляется обратно клиентскому приложению, для чего вызывается метод write:
os.write(buf, 0, lenght);
Методу write передается ссылка на массив, смещение начала данных в этом массиве, равное нулю, и размер принятого блока данных.
Для исключения задержек в передаче данных из-за накопления данных в буфере (при использовании буферизованных потоков) необходимо принудительно сбрасывать содержимое буфреа метдом flush:
os.flush();
И хотя в нашем случае мы не пользуемся буферизованными потоками, мы включили вызов этого метода для примера.
Теперь о завершающих действиях после прерывания цикла получения, отображения и передачи строк.
Наше приложение явням образом закрывает входной и выходной потоки данных, сокет, а также объект класса ServerSocket, с использованием которого был создан канал передачи данных:
is.close(); os.close(); s.close(); ss.close();
Исходный текст клиентского приложения SocketClient приведен в листинге 3.6.
Листинг 3.6. Файл SocketClient\SocketClient.java
// ========================================================= // Использование потоковых сокетов. // Приложение клиента // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.io.*; import java.net.*; import java.util.*; public class SocketClient { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Сокет для связи с сервером Socket s; // Входной поток для приема данных от сервера InputStream is; // Выходной поток для передачи данных серверу OutputStream os; try { // Выводим строку приглашения System.out.println("Socket Client Application" + "\nEnter any string or 'quit' to exit..."); } catch(Exception ioe) { // При возникновении исключения выводим его описание // на консоль System.out.println(ioe.toString()); } try { // Открываем сокет s = new Socket("localhost",9999); // Создаем входной поток для приема данных от сервера is = s.getInputStream(); // Создаем выходной поток для передачи данных серверу os = s.getOutputStream(); // Буфер для передачи данных byte buf[] = new byte[512]; // Размер принятого блока данных int length; // Рабочая строка String str; // Вводим команды и передаем их серверу while(true) { // Читаем строку команды с клавиатуры length = System.in.read(bKbdInput); // Если строка не пустая, обрабатываем ее if(length != 1) { // Преобразуем строку в формат String str = new String(bKbdInput, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(str, "\n"); str = new String((String)st.nextElement()); // Выводим передаваемую строку команды // на консоль для контроля System.out.println("> " + str); // Записываем строку в выходной поток, // передавая ее таким образом серверу os.write(bKbdInput, 0, length); // Сбрасываем буфер выходного потока os.flush(); // Принимаем ответ сервера length = is.read(buf); if(length == -1) break; // Отображаем принятую строку на консоли str = new String(buf, 0); st = new StringTokenizer(str, "\n"); str = new String((String)st.nextElement()); System.out.println(">> " + str); // Если введена строка 'quit', завершаем // работу приложения if(str.equals("quit")) break; } } // Закрываем входной и выходной потоки is.close(); os.close(); // Закрываем сокет s.close(); } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println( "Press <Enter> to terminate application..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Внутри метода main клиентского приложения SocketClient определены переменные для ввода строки с клавиатуры (массив bKbdInput), сокет s класса Socket для работы с сервером SocketServ, входной поток is и выходной поток os, которые связаны с сокетом s.
После вывода на консоль приглашающей строки клиентское приложение создает сокет, вызывая конструктор класса Socket:
s = new Socket("localhost",9999);
В процессе отладки мы запускали сервер и клиент на одном и том же узле, поэтому в качестве адреса сервера указана строка “localhost”. Номер порта сервера SocketServ равен 9999, поэтому мы и передали конструктору это значение.
После создания сокета наше клиентское приложение создает входной и выходной потоки, связанные с этим сокетом:
is = s.getInputStream(); os = s.getOutputStream();
Теперь клиентское приложение готово обмениваться данными с сервером.
Этот обмен выполняется в цикле, условием завершения которого является ввод пользователем строки “quit”.
Внутри цикла приложение читает строку с клавиатуры, записывая ее в массив bKbdInput:
length = System.in.read(bKbdInput);
Количество введенных символов сохраняется в переменной length.
Далее если пользователь ввел строку, а не просто нажал на клавишу <Enter>, эта строка отображается на консоли и передается серверу:
os.write(bKbdInput, 0, length); os.flush();
Сразу после передачи сбрасывается буфер выходного потока.
Далее приложение читает ответ, посылаемый сервером, в буфер buf:
length = is.read(buf);
Напомним, что наш сервер посылает клиенту принятую строку в неизменном виде.
Если сервер закрыл канал, то метод read возвращает значение -1. В этом случае мы прерываем цикл ввода и передачи строк:
if(length == -1) break;
Если же ответ сервера принят успешно, принятые данные записываются в строку str, которая отображается на консоли клиента:
System.out.println(">> " + str);
Перед завершением своей работы клиент закрывает входной и выходной потоки, а также сокет, на котором выполнялась передача данных:
is.close(); os.close(); s.close();
Как мы уже говорили, датаграммные сокеты не гарантируют доставку пакетов данных. Тем не менее, они работают быстрее потоковых и обеспечивают возможность широковещательной расслыки пакетов данных одновременно всем узлам сети. Последняя возможность используется не очень широко в сети Internet, однако в корпоративной сети Intranet вы вполне можете ей воспользоваться.
Для работы с датаграммными сокетами приложение должно создать сокет на базе класса DatagramSocket, а также подготовить объект класса DatagramPacket, в который будет записан принятый от партнера по сети блок данных.
Канал, а также входные и выходные потоки создавать не нужно. Данные передаются и принимаются методами send и receive, определенными в классе DatagramSocket.
Рассмотрим конструкторы и методы класса DatagramSocket, предназначенного для создания и использования датаграммных сокетов.
В классе DatagramSocket определены два конструктора, прототипы которых представлены ниже:
public DatagramSocket(int port); public DatagramSocket();
Первый из этих конструкторов позволяет определить порт для сокета, второй предполагает использование любого свободного порта.
Обычно серверные приложения работают с использованием какого-то заранее определенного порта, номер которого известен клиентским приложениям. Поэтому для серверных приложений больше подходит первый из приведенных выше конструкторов.
Клиентские приложения, напротив, часто применяют любые свободные на локальном узле порты, поэтому для них годится конструктор без параметров.
Кстати, с помощью метода getLocalPort приложение всегда может узнать номер порта, закрепленного за данным сокетом:
public int getLocalPort();
Прием и передача данных на датаграммном сокете выполняется с помощью методов receive и send, соответственно:
public void receive(DatagramPacket p); public void send(DatagramPacket p);
В качестве параметра этим методам передается ссылка на пакет данных (соответственно, принимаемый и передаваемый), определенный как объект класса DatagramPacket. Этот класс будет рассмотрен в следующем разделе нашей книги.
Еще один метод в классе DatagramSocket, которым вы будете пользоваться, это метод close, предназначенный для закрытия сокета:
public void close();
Напомним, что сборка мусора в Java выполняется только для объектов, находящихся в оперативной памяти. Такие объекты, как потоки и сокеты, вы должны закрывать после использования самостоятельно.
Перед тем как принимать или передавать данные с использованием методов receive и send вы должны подготовить объекты класса DatagramPacket. Метод receive запишет в такой объект принятые данные, а метод send - перешлет данные из объекта класса DatagramPacket узлу, адрес которого указан в пакете.
Подготовка объекта класса DatagramPacket для приема пакетов выполняется с помощью следующего конструктора:
public DatagramPacket(byte ibuf[], int ilength);
Этому конструктору передается ссылка на массив ibuf, в который нужно будет записать данные, и размер этого массива ilength.
Если вам нужно подготовить пакет для передачи, воспользуйтесь конструктором, который дополнительно позволяет задать адрес IP iaddr и номер порта iport адресата:
public DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport);
Таким образом, информация о том, в какой узел и на какой порт необходимо доставить пакет данных, хранится не в сокете, а в пакете, то есть в объекте класса DatagramPacket.
Помимо только что описанных конструкторов, в классе DatagramPacket определены четыре метода, позволяющие получить данные и информацию об адресе узла, из которого пришел пакет, или для которого предназначен пакет.
Метод getData возвращает ссылку на массив данных пакета:
public byte[] getData();
Размер пакета, данные из которого хранятся в этом массиве, легко определить с помощью метода getLength:
public int getLength();
Методы getAddress и getPort позволяют определить адрес и номер порта узла, откуда пришел пакет, или узла, для которого предназначен пакет:
public InetAddress getAddress(); public int getPort();
Если вы создаете клиент-серверную систему, в которой сервер имеет заранее известный адрес и номер порта, а клиенты - произвольные адреса и различные номера портов, то после получения пакета от клиента сервер может определить с помощью методов getAddress и getPort адрес клиента для установления с ним связи.
Если же адрес сервера неизвестен, клиент может посылать широковещательные пакеты, указав в объекте класса DatagramPacket адрес сети. Такая методика обычно используется в локальных сетях.
Как указать адрес сети?
Напомним, что адрес IP состоит из двух частей - адреса сети и адреса узла. Для разделения компонент 32-разрядного адреса IP используется 32-разрядная маска, в которой битам адреса сети соответствуют единицы, а битам адреса узла - нули.
Например, адрес узла может быть указан как 193.24.111.2. Исходя из значения старшего байта адреса, это сеть класса С, для которой по умолчанию используется маска 255.255.255.0. Следовательно, адрес сети будет такой: 193.24.111.0. Подробнее об адресации в сетях TCP/IP вы можете прочитать в 23 томе “Библиотеки системного программиста”, о котором мы упоминали в разделе “Передача данных с использованием сокетов” нашей книги.
Приложения DatagramServer и DatagramClient иллюстрируют применение датаграммных сокетов для передачи данных от нескольких копий одного и того же клиента одному серверу с известным адресом и номером порта.
Клиентские приложения посылают серверу строки, которые пользователь вводит с клавиатуры. Сервер принимает эти строки, отображая их в своем консольном окне вместе с номером порта клиента (рис. 3.4).
Рис. 3.4. Передача данных между приложениями DatagramClient и DatagramServer через датаграммный сокет
Когда с консоли клиента будет введена строка “quit”, этот клиент и сервер завершает свою работу. Работа остальных клиентов также может быть завершена подобным образом, причем независимо от того, работает сервер, или нет.
Наши клиенты не получают от сервера никакого подтверждения в ответ на переданные ему пакеты. Вы можете изменить программу клиента, добавив такую возможность. Однако учтите, что так как датаграммные сокеты не гарантируют доставки пакетов, ожидание ответа может продлиться бесконечно долго.
Чтобы избежать этого, ваше приложение должно выполнять работу с сервером в отдельной задаче, причем главная задача должна ждать ответ ограниченное время. Все что вам нужно, чтобы реализовать работу подобным образом, вы найдете в первой главе нашей книги, посвященной мультизадачности в приложениях Java.
Исходный текст приложения DatagramServer вы найдете в листинге 3.7.
Листинг 3.7. Файл DatagramServer\DatagramServer.java
// ========================================================= // Использование датаграммных сокетов // Приложение сервера // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.io.*; import java.net.*; import java.util.*; public class DatagramServer { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Буфер для чтения команд byte buf[] = new byte[512]; // Сокет сервера DatagramSocket s; // Принимаемый пакет DatagramPacket pinp; // Адрес узла, откуда пришел принятый пакет InetAddress SrcAddress; // Порт, откуда пришел принятый пакет int SrcPort; try { // Выводим строку приглашения System.out.println( "Datagramm Socket Server Application"); } catch(Exception ioe) { // При возникновении исключения выводим его описание // на консоль System.out.println(ioe.toString()); } try { // Создаем сокет сервера s = new DatagramSocket(9998); // Создаем пакет для приема команд pinp = new DatagramPacket(buf, 512); // Цикл обработки команд, полученных от клиента while(true) { // Принимаем пакет от клиента s.receive(pinp); // Получаем адрес узла, приславшего пакет SrcAddress = pinp.getAddress(); // Получаем порт, на котором был передан пакет SrcPort = pinp.getPort(); // Отображаем принятую команду на консоли сервера // Формируем строку из принятого блока String str = new String(buf, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(str, "\r\n"); str = new String((String)st.nextElement()); // Выводим строку команды на консоль System.out.println("> " + str + " < " + "port: " + SrcPort); // Если пришла команда 'quit', прерываем цикл if(str.equals("quit")) break; } // Закрываем сокет сервера s.close(); } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println( "Press <Enter> to terminate application..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Внутри функции main мы определили несколько переменных.
Массив bKbdInput хранит строку, введенную с клавиатуры.
Массив buf используется для хранения строк (команд), которые сервер получает от клиентских приложений.
В переменной s класса DatagramSocket хранится ссылка на датаграмный сокет, который будет использован для приема команд от клиентских приложений.
Переменная pinp класса DatagramPacket хранит пакеты, полученные сервером из сети.
Переменные SrcAddress (класса InetAddress) и SrcPort типа int хранят, соответственно, адрес и порт узла, отправившего пакет.
Первое, что делает сервер - это создание датаграммного сокета:
s = new DatagramSocket(9998);
Конструктору передается номер порта 9998, на котором сервер будет принимать пакеты данных от клиентских приложений.
После создания сокета сервер создает объекта класса DatagramPacket, в который будут записываться приходящие от клиентов пакеты:
pinp = new DatagramPacket(buf, 512);
Конструктор пакета получает ссылку на массив buf, в который нужно будет записывать приходящие по сети данные, и размер этого массива, равный 512 байт.
Далее сервер запускает цикл приема пакетов и отображения их содержимого.
Пакет принимается простым вызовом метода receive из класса DatagramSocket:
s.receive(pinp);
Этот метод блокирует работу вызвавшей его задачи до тех пор, пока по данному сокету не будет получен пакет данных.
Когда пакет будет получен, наше приложение определяет адрес и порт отправителя:
SrcAddress = pinp.getAddress(); SrcPort = pinp.getPort();
Эта информация может пригодиться, если вы будете посылать отправителю ответный пакет.
Наше приложение ответные пакеты не посылает. Оно преобразует принятые данные в текстовую староку класса String, добавляет к этой строке номер порта отправителя и отображает эту информацию на консоли:
System.out.println("> " + str + " < " + "port: " + SrcPort);
Цикл приема команд завершается, если от клиента пришла строка “quit”:
if(str.equals("quit")) break;
Перед тем как завершить свою работу, наше приложение закрывает датаграммный сокет, вызывая для этого метод close:
s.close();
В листинге 3.8 приведен исходный текст приложения DatagramClient.
Листинг 3.8. Файл DatagramClient\DatagramClient.java
// ========================================================= // Использование датаграммных сокетов // Приложение клиента // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.io.*; import java.net.*; import java.util.*; public class DatagramClient { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Размер введенной строки int length; // Рабочая строка String str; // Сокет клиента DatagramSocket s; // Передаваемый пакет DatagramPacket pout; try { // Выводим строку приглашения System.out.println( "Datagram Socket Client Application" + "\nEnter any string or 'quit' to exit..."); } catch(Exception ioe) { // При возникновении исключения выводим его описание // на консоль System.out.println(ioe.toString()); } try { // Получаем адрес локального узла InetAddress OutAddress = InetAddress.getLocalHost(); // Создаем сокет с использованием любого // свободного порта s = new DatagramSocket(); // Создаем передаваемый пакет pout = new DatagramPacket(bKbdInput, bKbdInput.length, OutAddress, 9998); // Цикл передачи команд серверу while(true) { // Читаем строку команды с клавиатуры length = System.in.read(bKbdInput); // Если строка не пустая, обрабатываем ее if(length != 1) { // Преобразуем строку в формат String str = new String(bKbdInput, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(str, "\n"); str = new String((String)st.nextElement()); // Выводим передаваемую строку команды // на консоль для контроля System.out.println("> " + str); // Посылаем пакет серверу s.send(pout); // Если введена команда 'quit', прерываем цикл if(str.equals("quit")) break; } } // Закрываем сокет s.close(); } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println( "Press <Enter> to terminate application..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Внутри метода main определен массив bKbdInput, предназначенный для хранения данных, введенных с клавиатуры, переменная length, в которой хранится размер этих данных, рабочая строка str класса String, датаграммный сокет s и пакет pout класса DatagramPacket.
Прежде всего приложение определяет адрес узла, на котором оно выполняется, вызывая метод getLocalHost:
InetAddress OutAddress = InetAddress.getLocalHost();
Этот адрес будет использован для формирования передаваемого пакета данных.
Затем клиент создает датаграммный сокет, применяя для этого конструктор без параметров:
s = new DatagramSocket();
Напомним, что в этом случае для сокета выделяется любой свободный порт.
На следующем шаге приложение формирует передаваемый пакет, вызывая конструктор класса DatagramPacket:
pout = new DatagramPacket(bKbdInput, bKbdInput.length, OutAddress, 9998);
Этому конструктору указывается адрес массива, содержащего введенные с клавиатуры данные, размер этого массива, адрес локального узла, на который нужно передать пакет, и номер порта серверного приложения.
Теперь все готово для запуска цикла передачи команд от клиента к серверу.
В этом цикле выполняется чтение строки с клавиатуры, причем размер прочитанной строки сохраняется в переменной length:
length = System.in.read(bKbdInput);
Далее, если строка не состоит из одного лишь символа перехода на новую строку, она отображается на косоли и посылается серверу методом send:
s.send(pout);
После того как пользователь введет строку “quit”, цикл завершается. Вслед за этим приложение закрывает датаграммный сокет:
s.close();
Итак, мы расказали вам, как приложения Java могут получать с сервера Web для обработки произвольные файлы, а также как они могут передавать данные друг другу с применением потоковых или датаграммных сокетов.
Однако наиболее впечатляющие возможности открываются, если организовать взаимодействие между приложением Java и расширением сервера Web, таким как CGI или ISAPI. В этом случае приложения или аплеты Java могли бы посылать произвольные данные расширению сервера Web для обработки, а затем получать результат этой обработки в виде файла.
О том, как сделать расширение сервера Web с применением интерфейса CGI или ISAPI вы можете узнать из 29 тома “Библиотеки системного программиста”, который называется “Сервер Web своими руками”. Если вы никогда раньше не создавали расширений сервера Web, мы настоятельно рекомендуем вам ознакомиться с этой книгой перед тем как продолжить работу над данным разделом.
Методика организации взаимодействия приложений Java и расширений сервера Web основана на применении классов URL и URLConnection.
Приложение Java, желающее работать с расширением сервера Web, создает объект класса URL для программы расширения (то есть для исполняемого модуля расширения CGI или библиотеки динамической компоновки DLL расширения ISAPI).
Далее приложение получает ссылку на канал передачи данных с этим расширением как объекта класса URLConnection. Затем, пользуясь методами getOutputStream и getInputStream из класса URLConnection, приложение создает с расширением сервера Web выходной и входной канал передачи данных.
Когда данные передаются приложением в выходной канал, созданный подобным образом, он попадает в стандартный поток ввода приложения CGI, как будто бы данные пришли методом POST из формы, определенной в документе HTML.
Обработав полученные данные, расширение CGI записывает их в свой стандартный выходной поток, после чего эти данные становятся доступны приложению Java через входной поток, открытый методом getInputStream класса URLConnection.
На рис. 3.5 показаны потоки данных для описанной выше схемы взаимодействия приложения Java и расширения сервреа Web с интерфейсом CGI.
Рис. 3.5. Взаимодействие приложения Java с расширением сервера Web на базе интерфейса CGI
Расширения ISAPI работают аналогично, однако они получают данные не из стандратного входного потока, а с помощью вызова специально предназначенной для этого функции интерфейса ISAPI. Вместо стандартного потока вывода также применяется специальная функция. Подробности вы можете узнать из 29 тома “Библиотеки системного программиста”.
Напомним, что в классе URL, рассмотренном нами в начале этой главы, мы привели прототип метода openConnection, возвращающий для заданного объекта класса URL ссылку на объект URLConnection:
public URLConnection openConnection();
Что мы можем получить, имея ссылку на этот объект?
Прежде всего, пользуясь этой ссылкой, мы можем получить содержимое объекта, адресуемое соответствующим объектом URL, методом getContent:
public Object getContent();
Заметим, что метод с таким же названием есть и в классе URL. Поэтому если все, что вы хотите сделать, это получение содержимое файла, адресуемое объектом класса URL, то нет никакой необходимости обращаться к классу URLConnection.
Метод getInputStream позволяет открыть входной поток данных, с помощью которого можно считать файл или получить данные от расширения сервера Web:
public InputStream getInputStream();
В классе URLConnection определен также метод getOutputStream, позволяющий открыть выходной поток данных:
public OutputStream getOutputStream();
Не следует думать, что этот поток можно использовать для записи файлов в каталоги сервера Web. Однако для этого потока есть лучшее применение - с его помощью можно передать данные расширению сервера Web.
Рассмотрим еще несколько полезных методов, определенных в классе URLConnection.
Метод connect предназначен для установки соединения с объектом, на который ссылается объект класса URL:
public abstract void connect();
Перед установкой соединения приложение может установить различные параметры соединения. Некоторые из методов, предназначенных для этого, приведены ниже:
// Включение или отключение кэширования по умолчанию public void setDefaultUseCaches(boolean defaultusecaches); // Включение или отключение кэширования public void setUseCaches(boolean usecaches); // Возможность использования потока для ввода public void setDoInput(boolean doinput); // Возможность использования потока для вывода public void setDoOutput(boolean dooutput); // Установка даты модификации документа public void setIfModifiedSince(long ifmodifiedsince);
В классе URLConnection есть методы, позволяющие определить значения параметров, установленных только что описанными методами:
public boolean getDefaultUseCaches(); public boolean getUseCaches(); public boolean getDoInput(); public boolean getDoOutput(); public long getIfModifiedSince();
Определенный интерес могут представлять методы, предназначенные для извлечения информации из заголовка протокола HTTP:
// Метод возвращает содержимое заголовка content-encoding // (кодировка ресурса, на который ссылается URL) public String getContentEncoding(); // Метод возвращает содержимое заголовка content-length // (размер документа) public int getContentLength(); // Метод возвращает содержимое заголовка content-type // (тип содержимого) public String getContentType(); // Метод возвращает содержимое заголовка date // (дата посылки ресурса в секундах с 1 января 1970 года) public long getDate(); // Метод возвращает содержимое заголовка last-modified // (дата изменения ресурса в секундах с 1 января 1970 года) public long getLastModified(); // Метод возвращает содержимое заголовка expires // (дата устаревания ресурса в секундах с // 1 января 1970 года) public long getExpiration();
Другие методы, определенные в классе URLConnection, позволяют получить все заголовки или заголовки с заданным номером, а также другую информацию о соединении. Мы не будем их рассматривать для экономии места в книге. При необходимости вы найдете описание этих методов в справочной системе Microsoft Visual J++.
В 29 томе “Библиотеки системного программиста” мы рассказывали о том, как с помощью расширений сервера Web, выполненных на основе интерфейса CGI и ISAPI можно обрабатывать данные из форм, расположенных в документах HTML. В частности, мы привели там исходные тексты программы controls.exe (составленной на языке программирования С), которая динамически создавала и отображала данные, введенные в форме. Внешний вид этой формы показан на рис. 3.6, воспроизведенном нами из указанного тома.
Рис. 3.6. Форма для ввода данных
Программа CGI controls.exe получала данные, введенные пользователем в этой форме, после чего динамически создавала документ HTML, в котором отображала состояние переменных серды, полученные данные в исходном и раскодированном виде, а также список значений полей (рис. 3.7).
Рис. 3.7. Документ HTML, сформрованный динамически программой CGI control.exe
Создавая приложение CallCGI, мы поставили перед собой задачу заменить форму приложением Java, которое вводит с клавиатуры текстовую строку полей и передает ее программе CGI controls.exe. Содержимое динамически сформированного программой CGI документа HTML приложение CallCGI отображает в своем консольном окне, как это показано на рис. 3.8.
Рис. 3.8. Отображение в окне приложения Java содержимого документа HTML, полученного от программы CGI
Исходный текст приложения CallCGI приведен в листинге 3.9.
Листинг 3.9. Файл CallCGI\CallCGI.java
// ========================================================= // Вызов расширения сервера Web на базе интерфейса CGI // из приложения Java // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.io.*; import java.net.*; import java.util.*; public class CallCGI { // ------------------------------------------------------- // main // Метод, получающий управление при запуске приложения // ------------------------------------------------------- public static void main(String args[]) { // Массив для ввода строки с клавиатуры byte bKbdInput[] = new byte[256]; // Размер принятого блока данных int length; // Рабочая строка String str; // Адрес URL вызываемой программы CGI URL u; // Канал связи с расширением CGI URLConnection c; // Выходной поток для передачи данных расширению CGI PrintStream ps; // Входной поток для получения данных от расширения CGI DataInputStream is; try { // Выводим строку приглашения System.out.println("CGI extension call" + "\nEnter any string for send to CGI..."); // Читаем строку, передаваемую расширению, // с клавиатуры length = System.in.read(bKbdInput); // Если строка не пустая, обрабатываем ее if(length != 1) { // Преобразуем строку в формат String str = new String(bKbdInput, 0); // Обрезаем строку, удаляя символ конца строки StringTokenizer st; st = new StringTokenizer(str, "\n"); str = new String((String)st.nextElement()); // Выполняем кодировку URL для передаваемой строки String StrEncoded = URLEncoder.encode(str); // Отображаем перекодированную строку System.out.println("Encoded string: >" + StrEncoded + "<"); // Создаем объект класса URL для расширения CGI u = new URL( "http://frolov/frolov-cgi/controls.exe"); // Открываем канал связи с расширением CGI c = u.openConnection(); // Создаем выходной поток данных для передачи // введенной строки серверу CGI ps = new PrintStream(c.getOutputStream()); // Передаем закодированную строку расширению CGI ps.println(StrEncoded); // Закрываем выходной поток ps.close(); // Создаем входной поток для приема данных от // расширения CGI is = new DataInputStream(c.getInputStream()); System.out.println( "\n------------------------------------------" + "\n Data from CGI extension" + "\n------------------------------------------\n"); // Прием данных выполняем в цикле while (true) { // Получаем очередную строку str = is.readLine(); // Если последняя строка, прерываем цикл if(str == null) break; // Отображаем принятую строку System.out.println(str); } // Закрываем входной поток is.close(); } } catch(Exception ioe) { System.out.println(ioe.toString()); } try { System.out.println( "Press <Enter> to terminate application..."); System.in.read(bKbdInput); } catch(Exception ioe) { System.out.println(ioe.toString()); } } }
Внутри метода main мы определили несколько переменных.
Массив bKbdInput предназначен для хранения строки, введенной с помощью клавиатуры. В переменную length записывается длина этой строки.
Строка str класса String используется в качестве рабочей.
Переменная u класса URL предназначена для хранения ссылки на объект URL, созданный для загрузочного файла программы CGI.
Ссылка на канал связи с программой CGI хранится в переменной с именем c класса URLConnection.
Переменные ps класса PrintStream и is класса DataInputStream хранят ссылки, соответственно, на выходной и входной потоки, через которые наше приложение обменивается данными с программой CGI.
После вывода приглашения на консоль наша программа вводит строку, которая будет передана программе CGI:
length = System.in.read(bKbdInput);
Далее массив bKbdInput преобразуется в строку str и перекодируется в кодировку URL. Эта кодировка была описана нами в 29 томе “Библиотеки системного программиста”. Она выполняется с помощью статического метода encode, определенного в классе URLEncoder:
String StrEncoded = URLEncoder.encode(str);
Перекодированная строка отображается на консоли:
System.out.println("Encoded string: >" + StrEncoded + "<");
На следующем этапе наше приложение создает объект класса URL для загрузочного файла программы CGI:
u = new URL("http://frolov/frolov-cgi/controls.exe");
Здесь предполагается, что программа CGI находится в файле controls.exe, который записан в виртуальный каталог frolov-cgi на сервере Web с адресом http://frolov). Про создание и настройку виртуальных каталогов для размещения расширений сервера Web мы рассказали в 29 томе “Библиотеки системного программиста”.
После создания объекта класса URL мы создаем канал с программой CGI как объект класса URLConnection:
c = u.openConnection();
Пользуясь этим каналом, мы вначале получаем выходной поток методом getOutputStream, а затем на его базе создаем форматированный выходной поток класса PrintStream, удобный для записи в него текстовых строк:
ps = new PrintStream(c.getOutputStream());
Через канал ps наше приложение передает программе CGI строку StrEncoded, а затем закрывает выходной поток, как это показано ниже:
ps.println(StrEncoded); ps.close();
В этот момент на сервере Web уже запущена программа CGI, и она приняла переданные ей данные. Обработав эти данные, программа CGI записала в стандартный выходной поток динамически созданный ей документ HTML.
Для получения документа наше приложение CallCGI создала входной форматированный поток данных:
is = new DataInputStream(c.getInputStream());
Входной поток создан в два приема. Вначале с помощью метода getInputStream приложение создала обычный входной поток, а затем, на его базе, форматированный входной поток класса DataInputStream.
Получение от программы CGI динамически сформированного ей документа HTML наше приложение выполняет в цикле по строкам.
Строка документа HTML читается из входного форматированного потока методом readLine и записывается в переменную str:
str = is.readLine();
Если в процессе чтения был достигнут конец потока, цикл прерывается:
if(str == null) break;
Строка, полученная методом readLine, отображается на консоли пиложения:
System.out.println(str);
После завершения цикла входной поток закрывается методом close:
is.close();
В лситинге 3.10 мы привели исходный текст программы CGI с именем controls. Он несколько упрощен по сравнению с исходным текстом одноименного приложения, описанного в 29 томе “Библиотеки системного программиста” - мы выбросили обработку метода передачи данных GET, так как наше приложение CallCGI передает данные только методом POST. Описание этой программы вы найдете в упомянутом 29 томе.
Листинг 3.10. Файл controls\controls.c
// =============================================== // Программа CGI controls.c // Демонстрирует методы получения и обработки // данных от форм, расположенных в документах HTML // // (C) Фролов А.В., 1997 // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // =============================================== #include <stdio.h> #include <stdlib.h> #include <string.h> // Прототипы функций перекодировки void DecodeStr(char *szString); char DecodeHex(char *str); // ------------------------------------------------ // Функция main // Точка входа программы CGI // ------------------------------------------------ void main(int argc, char *argv[]) { int lSize; FILE * fileReceived; char * szMethod; char szBuf[8196]; char szSrcBuf[8196]; char * szPtr; char * szParam; // Вывод заголовка HTTP и разделительной строки printf("Content-type: text/html\n\n"); // Вывод начального форагмента документа HTML, // формируемого динамически printf("<!DOCTYPE HTML PUBLIC" " \"-//W3C//DTD HTML 3.2//EN\">"); printf("<HTML><HEAD><TITLE>Call CGI from Java" "</TITLE></HEAD><BODY BGCOLOR=#FFFFFF>"); // Определяем метод передачи данных szMethod = getenv("REQUEST_METHOD"); // Обработка метода POST if(!strcmp(szMethod, "POST")) { // Определяем размер данных, полученных от навигатора // при передаче данных из полей формы lSize = atoi(getenv("CONTENT_LENGTH")); // Читаем эти данные в буфер szBuf из // стандартного потока ввода STDIN fread(szBuf, lSize, 1, stdin); // Создаем файл, в который будут записаны // принятые данные fileReceived = fopen("received.dat", "w"); // Выполняем запись принятых данных fwrite(szBuf, lSize, 1, fileReceived); // Закрываем файл принятых данных fclose(fileReceived); // Отображаем значения некоторых переменных среды printf("<H2>Environment variables</H2>"); // Метод доступа printf("REQUEST_METHOD = %s", getenv("REQUEST_METHOD")); // Размер полученных данных в байтах printf("<BR>CONTENT_LENGTH = %ld", lSize); // Тип полученных данных printf("<BR>CONTENT_TYPE = %s", getenv("CONTENT_TYPE")); // Закрываем буфер данных двоичным нулем, // превращая его таким образом в строку szBuf[lSize] = '\0'; // Делаем копию принятых данных в буфер szSrcBuf strcpy(szSrcBuf, szBuf); // Отображаем принятые данные без обработки printf("<H2>Received data</H2>"); printf("<P>%s", szSrcBuf); // Выполняем перекодировку принятых данных DecodeStr(szSrcBuf); // Отображаем результат перекодировки printf("<H2>Decoded data</H2>"); printf("<P>%s", szSrcBuf); // Выводим список значений полей формы printf("<H2>Filds list</H2>"); // Дописываем в конец буфера принятых данных // символ "&", который используется в качестве // разделителя значений полей szBuf[lSize] = '&'; szBuf[lSize + 1] = '\0'; // Цикл по полям формы for(szParam = szBuf;;) { // Ищем очередной разделитель szPtr = strchr(szParam, '&'); // Если он найден, раскодируем строку параметров if(szPtr != NULL) { *szPtr = '\0'; DecodeStr(szParam); // Выводим в документ значение параметра printf("%s<BR>", szParam); // Переходим к следующему параметру szParam = szPtr + 1; // Если достигнут конец буфера, завершаем цикл if(szParam >= (szBuf + lSize)) break; } else break; } // Выводим завершающий фрагмент документа HTML printf("</BODY></HTML>"); return; } } // ------------------------------------------------ // Функция DecodeStr // Раскодирование строки из кодировки URL // ------------------------------------------------ void DecodeStr(char *szString) { int src; int dst; char ch; // Цикл по строке for(src=0, dst=0; szString[src]; src++, dst++) { // Получаем очередной символ перекодируемой строки ch = szString[src]; // Заменяем символ "+" на пробел ch = (ch == '+') ? ' ' : ch; // Сохраняем результат szString[dst] = ch; // Обработка шестнадцатеричных кодов вида "%xx" if(ch == '%') { // Выполняем преобразование строки "%xx" // в код символа szString[dst] = DecodeHex(&szString[src + 1]); src += 2; } } // Закрываем строку двоичным нулем szString[dst] = '\0'; } // ------------------------------------------------ // Функция DecodeHex // Раскодирование строки "%xx" // ------------------------------------------------ char DecodeHex(char *str) { char ch; // Обрабатываем старший разряд if(str[0] >= 'A') ch = ((str[0] & 0xdf) - 'A') + 10; else ch = str[0] - '0'; // Сдвигаем его влево на 4 бита ch <<= 4; // Обрабатываем младший разряд и складываем // его со старшим if(str[1] >= 'A') ch += ((str[1] & 0xdf) - 'A') + 10; else ch += str[1] - '0'; // Возвращаем результат перекодировки return ch; }