Проблемы клиент-серверной системы

Если создается клиент-серверная система, в которой сервер имеет заранее известный адрес и номер порта, а клиенты - произвольные адреса и различные номера портов, то после получения пакета от клиента сервер может определить с помощью методов getAddress() и getPort() адрес клиента для установления с ним связи.

Если адрес сервера неизвестен, клиент может получать широковещательные пакеты, указав в объекте класса DatagramPacket адрес сети.

Как узнать адрес сети? Адрес IP состоит из двух частей - адреса сети и адреса узла. Для разделения компонент 32-разрядного адреса IP используется 32-разрядная маска, в которой битам адреса сети соответствуют единицы, а битам узла - нули.

Например, например адрес узла в сети может быть указан как 194.84.124.60. Исходя из значения старшего байта адреса, это есть сеть класса C, для которой по умолчанию используется маска 255.255.255.0. Следовательно, адрес сети будет 194.84.124.0.

4.3 Пример использования датаграммных сокетов

Рассмотрим в качестве примера два приложения SеrverDatagram и ClientDatagram (пример 2), работающих с датаграммными сокетами. Одно из них выполняет роль сервера, а другое служит клиентом. Эти приложения иллюстрируют применение датаграммных сокетов для передачи данных от нескольких копий одного и того же клиентского приложения одному серверу с его известными адресом и номером порта.

Клиентские приложения посылают серверу строки, которые пользователь вводит с клавиатуры. Сервер принимает эти строки, отображая их в своем консольные вместе с номером порта клиента и его адресом. Когда с консоли клиента будет введена строка «QUIT» (или «quit»), этот клиент и сервер завершают свою работу. Работа других клиентов может быть завершена подобным образом, причем независимо от того, работает север или нет.

/*------------- Пример 2. Файл ServerDatagram.java -------------*/

import java.io.*;

import java.net.*;

import java.util.*;

class ServerDatagram

{ public static void main(String args[])

{ boolean quit=false;

String str; StringTokenizer strFull;

System.out.println("Server Application");

System.out.println("---- Start ----");

byte buf[]=new byte[512]; // буфер для данных

InetAddress srcAdd; // адрес узла, откуда пришел пакет

int srcPort; // порт, откуда пришел пакет

try

{ // создание сокета сервера

DatagramSocket s=new DatagramSocket(9999);

// создание пакета для приема команд

DatagramPacket pIn=new DatagramPacket(buf,512);

// цикл получения команд от клиента

while(!quit)

{ // ---- получение данных от клиентов

s.receive(pIn); // получение пакета

srcAdd=pIn.getAddress(); // адрес узла-отправителя

srcPort=pIn.getPort(); // порт узла-отправителя

// ---- при получении "quit" - выход

// удаление незначащих байт из массива

strFull=new StringTokenizer(new String(buf,0),"\r\n");

str=(String)strFull.nextElement();

if(str.toLowerCase().equals("quit")) quit=true;

// ---- вывод полученной от клиента строки

str="Address:"+srcAdd.toString()+

" Port:"+srcPort+" String:"+str;

System.out.println(str);

}

s.close(); // закрытие сокета

}

catch(IOException ioe)

{ System.out.println(ioe.toString());

}

System.out.println("---- Finish ----");

}

}

/*------------- Пример 2. Файл ClientDatagram.java -------------*/

import java.io.*;

import java.net.*;

import java.util.*;

class ClientDatagram

{ public static void main(String args[])

{ boolean quit=false; int length;

String str; StringTokenizer strFull;

System.out.println("Client Application");

System.out.println("Enter any string or 'quit' to exit...");

byte buf[]=new byte[512]; // буфер для передачи данных

try

{ // создание сокета клиента с

// использованием свободного порта

DatagramSocket s=new DatagramSocket();

// адрес узла, на котором запущен сервер

// в процессе отладки сетевых приложений, когда

// сервер и клиент работают на одном узле,

// в качестве адреса сервера используется адрес

// данного локального узла, выдаваемый методом

// InetAddress.getLocalHost();

InetAddress serverAdd=InetAddress.getLocalHost();

// в рабочей версии клиента указывается IP-адрес сервера

//InetAddress serverAdd=

// InetAddress.getByName("194.84.124.60");

//InetAddress serverAdd=InetAddress.getByName("gosha");

//InetAddress serverAdd=

// InetAddress.getByName("gosha.vvsu.ru");

// создание пакета для передачи команд серверу

DatagramPacket pOut=

new DatagramPacket(buf,512,serverAdd,9999);

// цикл посылки команд серверу

while(!quit)

{ // ввод строки с клавиатуры

length=System.in.read(buf);

if(length==1)

// только символ перевода строки

continue;

// ---- посылка данных

s.send(pOut); // посылка пакета

// ---- при вводе "quit" - выход

// удаление незначащих байт из массива

strFull=new StringTokenizer(new String(buf,0),"\r\n");

str=(String)strFull.nextElement();

if(str.toLowerCase().equals("quit")) quit=true;

}

s.close(); // закрытие сокета

}

catch(IOException ioe)

{ System.out.println(ioe.toString());

}

}

}

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

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

5 Приложения ServerSocketApp и ClientSocketApp

Задание. На основе текстов приложений Server-Client (пример 1) создать приложения ServerSocketApp-ClientSocketApp, иллюстрирующие работу с потоковыми сокетами.

До ввода пользователем строки «QUIT» (или «quit») приложение ClientSocketApp должно в цикле: через свой входной поток получать от сервера количество переданных серверу строк (массивов байт); печатать это число; давать возможность пользователю ввести строку на консоли, помещая ее в массив байт; передавать этот массив байт серверу посредством записи его в свой выходной поток; преобразовывать массив байт в строку и в случае получения строки «QUIT» (или «quit») прекращать свою работу.

В свою очередь приложение ServerSocketApp в цикле должно: считывать из своего входного потока массив байт, переданный ему клиентом; преобразовывать массив байт в строку; печатать строку; в случае получения строки «QUIT» (или «quit») прекращать свою работу, иначе увеличивать количество переданных ему массивов байт и записывать это число в свой выходной поток для передачи его клиенту.

Методические указания.При создании приложений ServerSocketApp-ClientSocketApp воспользоваться методами ввода с консоли массива байт, создания строки на основе этого массива байт и выделения значащих символов из строк, как это сделано в приложениях ServerDatagram-ClientDatagram (пример 2).

Метод main() приложения ClientSocketApp (модификация метода main() приложения Client)

Добавим описание следующих переменных:

byte buf[]=new byte[512]; // буфер для передачи данных

String str; StringTokenizer strFull; // для выделения из буфера строки

В качестве запуска процесса передачи данными поместим в выходной поток клиентаdataOut для передачи серверу не нулевой символ, как раньше, а строку "\r\n", вызывая для объекта dataOut последовательно методы writeChars() и flush().

В цикле до тех пор, пока переменная quit не равна true, сделаем следующие действия:

· чтение целого числа из входного потока клиента dataIn методом readInt();

· печать этого числа методом System.out.println();

· чтение байтов, вводимых пользователем с консоли, в буфер buf методом System.in.read();

· если количество прочитанных байт (выдаваемых методом read()) равно 1 (введен только символ перевода строки), то перейти к следующей итерации цикла;

· удалить незначащие байты массива buf, помещая полученную строку в str (см. приложение ClientDatagram);

· если строка str равна строке «QUIT» (или «quit»), то присвоить переменной quit значение true;

· поместить в выходной поток клиента dataOut для передачи серверу массив байтов buf, вызывая для объекта dataOut последовательно методы write() и flush().

Метод main() приложения ServerSocketApp (модификация метода main() приложения Server)

Добавим описание следующих переменных:

byte buf[]=new byte[512]; // буфер для получаемых данных

String str; StringTokenizer strFull; // для выделения из буфера строки

В цикле до тех пор, пока переменная quit не равна true, сделаем следующие действия:

· чтение массива байт, переданного клиентом, из входного потока сервера dataIn методом read() в массив buf;

· удалить незначащие байты массива buf, помещая полученную строку в str (см. приложение ServerDatagram);

· печать полученной строки методом System.out.println();

· если строка str равна строке «QUIT» (или «quit»), то присвоить переменной quit значение true;

· иначе увеличить счетчик count; передать значение count клиенту, вызывая для объекта dataOut последовательно методы writeInt() и flush().

Задания к лабораторной работе

Задание 1. Проверить и объяснить работу приложений Server-Client,ServerDatagram-ClientDatagram, рассматриваемых в данной главе в качестве примеров и отмеченных курсивом.

Задание 2.Создать приложения ServerSocketApp и ClientSocketAppи объяснить их работу.

Задание 3. Дать ответы на контрольные вопросы.

Контрольные вопросы

1. Что такое сокеты?

2. Какие типы сокетов существуют, чем они отличаются друг от друга?

3. Какое преимущество имеют потоковые сокеты?

4. Что такое IP-адрес и доменный адрес узла (хоста)?

5. Какой класс Java предназначен для работы с IP-адресами?

6. Как создать объект этого класса для локального и для удаленного узлов?

7. Каким образом задается адрес узла при создании объекта, отвечающего за адрес IP?

8. Какова последовательность действий приложений Java, необходимая для создания канала и передачи данных между клиентским и серверным приложением?

9. Почему приложение должно самостоятельно закрывать все потоки ввода-вывода?

10. Каковы недостатки и преимущества датаграммных сокетов?

11. Что должны сделать приложения для работы с датаграммами?

12. Какие методы применяются для посылки и получения датаграмм?

13. Какой класс отвечает за пакет пересылаемых или получаемых данных? Какую информацию содержит объект этого класса?

14. Как серверное приложение может определить адрес клиентского приложения, приславшего датаграмму?

15. Если адрес сервера неизвестен клиентскому приложению, то как оно может получать его широковещательные пакеты (датаграммы)?

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

 

ЛАБОРАТОРНАЯ РАБОТА № 9

СВЯЗЬ ПО СЕТИ С ПОМОЩЬЮ URL (2 ЧАСА)

МЕТОДИЧЕСКИЕ УКАЗАНИЯ К ЛАБОРАТОРНОЙ РАБОТЕ

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

1. Универсальный адрес ресурсов URL

Для ссылки на ресурсы сети Internet применяется так называемый универсальный адрес ресурсов URL (Universal Resource Locator). В общем виде этот адрес выглядит следующим образом:

[protocol://]host[:port][path]

Строка адреса начинается с протокола protocol, который должен быть использован для доступа к ресурсу. Документы HTML, например, передаются с сервера Web удаленным пользователям с помощью протокола HTTP. Файловые серверы в сети Internet работают с протоколом FTP. Наиболее распространенные протоколы перечислены в следующей таблице:

Описание Протокол
HyperText Transfer Protocol (HTTP) http://
File Transfer Protocol (FTP) ftp://
Wide Area Index and Search (WAIS) wais://
Telnet telnet://
Usenet (News) new://
Simple Mail Transfer Protocol (SMTP) mailto:
Протокол для работы с локальными файлами file://

 

Параметр host обязательный. Он должен быть указан как доменный адрес или как адрес IP (в виде четырех десятизначных чисел). Например:

http://www.vvsu.ru

http://194.84.124.12

Необязательный параметр port задает номер порта для работы с сервером. По умолчанию для протокола HTTP используется порт с номером 80, однако для специализированных серверов это может быть и не так.

Номер порта идентифицирует программу, работающую в узле сети TCP/IP и взаимодействующую с другими программами, расположенными на том же или другом узле сети. Например, при разработке программы, передающей данные через сеть TCP/IP с использованием интерфейса сокетов, при создании канала с удаленным компьютером необходимо указать не только адрес IP, но и номер порта, который будет использован для передачи данных. Ниже показано, как нужно указывать в адресе URL номер порта:

http://www.vvsu.ru:8082

Рассмотрим теперь параметр path, определяющий путь к объекту. Если в качестве адреса URL указать навигатору только доменное имя сервера, сервер перешлет навигатору свою главную страницу. Имя файла этой страницы зависит от сервера. Большинство серверов на базе операционной системы UNIX посылают по умолчанию файл документа с именем index.html. Сервер Microsoft Information Server может использовать для этой цели имя default.htm или любое другое, определенное при установки сервера.

Для ссылки на конкретный документ HTML или на файл любого другого объекта необходимо указать в адресе URL его путь, включающий имя файла, например:

http://www.vvsu.ru/cts/rus_win/index.html

http://www.vvsu.ru/cts/teachers/arhipova/pictures/clock.gif

Корневой каталог сервера Web обозначается символов /. Если путь вообще не задан по умолчанию используется корневой каталог.

2. Класс java.net.URL в библиотеке классов Java

Для работы с ресурсами, заданными своими адресами URL, в библиотеке классов Java имеется очень удобный и мощный класс с названием URL. Инкапсулируя в себе достаточно сложные процедуры сетевого программирования, класс URL предоставляет небольшой набор простых в использовании конструкторов и методов. Работать с этим классом могут как автономные приложения, так и апплеты

Конструкторы класса URL

Класс URL содержит четыре конструктора. Первый из них создает объект URL в виде сетевого ресурса, адрес URL которого передается конструктору в виде текстовой строки через параметр spec.

Public URL(String spec);

В процессе создания объекта проверяется заданный адрес URL, а также наличие указанного в нем ресурса. Если адрес указан неверно или заданный в нем ресурс отсутствует, возникает исключение MalformedURLException. Это же исключение возникает при попытке использовать протокол, с которым данная система не может работать.

Второй вариант конструктора допускает раздельное указание протокола, адреса узла, номера порта, а также имени файла:

public URL(String protocol, String host, int port, String file);

Третий вариант предполагает использование номера порта, принятого по умолчанию (для протокола HTTP это порт с номером 80):

public URL(String protocol, String host, String file);

Четвертый вариант конструктора допускает указание контекста адреса URL и строки адреса URL:

public URL(URL context, String spec);

Этот конструктор создает URL каталога или файла по его пути spec относительно заданной URL-ссылки context.

Хотя фирма Sun разработала поддержку URL для очень ограниченного числа протоколов – DOC, FILE и HTTP (протокол FILE URL применяется для локальных файлов, а DOC URL использован в броузере Hotjava), следует отметить, что в классе URL имеется возможность создания поддержки других протоколов.

Некоторые методы класса URL

С помощью метода getHost() можно определить имя узла, соответствующего данному объекту URL. Метод getFile() позволяет получить информацию о файле, связанном с данным объектом URL. Метод getPort() предназначен для определения номера порта, на котором выполняется связь для объекта URL. С помощь метода getProtocol() можно определить протокол, с использованием которого установлено соединение с ресурсом, заданным объектом URL.

С помощью метода sameFile() можно определить, ссылаются ли два объекта класса URL на один и тот же ресурс. Для определения идентичности двух адресов можно также воспользоваться методом equals().

Для доступа к ресурсам и их содержимому используются методы openStream(), getContent(), openConnection().

3. Использование класса java.net.URL

После реализации класса URL часто бывает необходимо получить доступ к ресурсам, на которые он указывает. Класс URL предлагает для этого три основных метода: openStream(); getContent(); openConnection(). Рассмотрим эти методы подробнее.

3.1 Чтение из потока класса InputStream, полученного от объекта класса URL

Мeтод openStream() позволяет создать входной поток класса InputStream для чтения файла ресурса, связанного с созданным объектом класса URL. Для выполнения операции чтения из созданного таким образом потока можно использовать любую разновидность метода read(), определенных в классе InputStream. После использования потока его следует закрыть методом close() класса InputStream.

Пару методов (openStream() из класса URL и read() класса InputStream) можно применить для решения задачи получения содержимого двоичного или текстового файла (например, HTML-файл), хранящегося в одном из каталогов сервера Web. После этого обычное приложение или апплет может выполнить локальную обработку полученного файла на компьютере удаленного пользователя.

Например, рассмотрим фрагмент апплета, в котором создается URL к файлу, расположенному на WWW-сервере апплета (откуда апплет загружен на удаленный компьютер), затем открывается поток, связанный с этим файлом, из которого потом читается содержимое HTML-файла:

URL myUrl;

try

{ myUrl=new URL(getCodeBase(),"index.html");

} catch(MalformedURLException e) { /* обработка исключения */ }

try

{ InputStream InStream=myUrl.openStream();

// чтение данных из потока InStream

InStream.close();

} catch(IOException e) { /* обработка исключения */ }

Приведем в качестве примера приложение UrlOpenStream (пример 1), в котором при помощи входного потока считывается html-файл из каталога удаленного сервера Web и его содержимое выводится на консоль:

/*------------- Пример 1. Файл UrlOpenStream.java -------------*/

import java.net.*;

import java.io.*;

class UrlOpenStream

{ public static void main(String args[])

{ URL Url;

try

{// создание URL файла и открытие

// входного потока, связанного с этим файлом

Url=new URL("http://www.microsoft.com");

InputStream InStream=Url.openStream();

// чтение данных из потока InStream

int b;

while ((b=InStream.read())!=-1)

{ System.out.print(""+(char)b);

}

InStream.close(); // закрытие потока

}

catch(Exception e) { System.out.println(e.toString()); }

}

}

3.2 Получение содержимого файла, связанного с объектом класса URL

Метод getContent() открывает поток к ресурсу в точности также, как это делает метод openStream(), но затем пытается определить MIME потока (тип файла) и конвертировать поток в объект Java. Зная тип MIME потока данных, URL может передать поток данных методу, созданному для работы именно с этим типом данных. Этот метод должен выдать данные, инкапсулированные в соответствующем типе объекта Java. Например, если создан URL, указывающий на изображение в формате GIF, метод getContent() должен понять, что поток относится к типу MIME «image/gif», и вернуть экземпляр класса Image. Объект Image будет содержать копию GIF-картинки. Для того, чтобы метод getContent() вернул объект в соответствии с MIME, необходимо определить для этого MIME собственный класс ContentHandler, который в конечном счете и обрабатывает данные ресурса, когда вызывается метод getContent() класса URLили URLConnection.

Практически, можно использовать метод getContent() для получения текстовых файлов, расположенных в сетевых каталогах. К сожалению, метод getContent() непригоден для получения документов HTML, так как для данного ресурса не определен обработчик содержимого, предназначенный для создания объекта. Метод getContent не способен создать объект из чего-либо другого, кроме как из текстового файла.

Приведем пример использования метода getContent() в приложении. Сначала создается объект URL, потом вызывается метод getContent(), чтобы восстановить ресурс в объекте Java, а затем применяется операция instanceof для определения того, какой тип объекта возвращен:

URL myUrl; Object obj;

try

{ myUrl=new URL

("http://www.vvsu.ru/cts/teachers/arhipova/t.txt");

} catch(MalformedURLException e) { /* обработка исключения */ }

try

{ obj=myUrl.getContent();

} catch(IOException e) { /* обработка исключения */ }

if(obj instanceof String) { /* действия, если строка */ }

else { /* тип неизвестен, действия по умолчанию */ }

Рассмотрим в качестве примера апплет UrlGetContent (пример 2), в котором получается содержимое файла из каталога сервера Web (с которого загружен апплет). В случае, если файл содержит простой текст, то содержимое файла выводится в текстовую область, введенную в апплет:

/*------------- Пример 2. Файл UrlGetContent.java -------------*/

import java.applet.*;

import java.awt.*;

import java.net.*;

public class UrlGetContent extends Applet

{ String S; Object obj;

public void init()

{ resize(600, 400);

// создание текстовой области и введение ее в апплет

TextArea text=new TextArea(30,80);

add(text);

URL Url;

try

{ // создание URL файла с сервера апплета

// и получение содержимого этого файла

Url=new URL(getCodeBase(),"UrlGetContent.java");

obj=Url.getContent();

// проверка, является ли содержимое текстом

if(obj instanceof String) { S=(String)obj; }

else { S="Unknown object"; }

}

catch(Exception e) { S=e.toString(); }

// вывод либо содержимого файла, либо предупреждения

text.setText(S);

}

public void paint(Graphics g){}

}

4. Соединение с помощью объекта класса URLConnection

Если создается приложение, которое позволяет читать из каталога сервера WWW текстовые или двоичные файлы, то можно создать поток методом openStream() или получить содержимое файла методом getContent(). Однако есть и другая возможность.