Работа в WWW
Среди программного обеспечения Internet большое распространение получила информационная система WWW (World Wide Web), основанная на прикладном протоколе HTTP (Hypertext Transfer Protocol). В ней используется расширенная адресация, называемая URL (Uniform Resource Locator). Эта адресация имеет такие схемы:
protocol://authority@host:port/path/file#ref
protocol://authority@host:port/path/file/extra_path?info
Здесь необязательная часть authority — это пара имя:пароль для доступа к хосту, host — это IP-адрес или доменное имя хоста. Например:
http://132.192.5.10:8080/public/some.html
ftp://guest:password@lenta.ru/users/local/pub
ffle:///C:/text/html/index.htm
Если какой-то элемент URL отсутствует, то берется стандартное значение. Например, в первом примере номер порта port равен 80, а имя файла path — какой-то головной файл, определяемый хостом, чаще всего это файл с именем index.html. В третьем примере номер порта равен 21. В последнем примере в форме URL просто записано имя файла index.htm, расположенного на разделе С: жесткого диска той же самой машины.
В Java для работы с URL есть класс URL пакета java.net. Объект этого класса создается одним из шести конструкторов. В основном конструкторе
URL(String url)
задается расширенный адрес url в виде строки. Кроме методов доступа getxxxo, позволяющих получить элементы URL, в этом классе есть два интересных метода:
openConnection () — определяет связь с URL и возвращает объект класса URLConnection; openStream() — устанавливает связь с URL и открывает входной поток в виде возвращаемого объекта класса inputstream.Листинг 19.1 показывает, как легко можно получить файл из Internet, пользуясь методом openStream().
Листинг 19.1. Получение Web-страницы
import java.net.*;
import j ava.io.*;
class SimpleURL{
public static void main(String[] args){
try{
URL bhv = new URL(" http://www.bhv.ru/ ");
BufferedReader br = new BufferedReader(
new InputStreamReader(bhv.openStream()));
String line;
while ((line = br.readLine()) != null)
System.out.println(line);
br.close();
}catch(MalformedURLException me){
System.err.println("Unknown host: " + me);
System.exit(0);
}catch(IOException ioe){
System.err.println("Input error: " + ioe);
}
}
}
Если вам надо не только получить информацию с хоста, но и узнать ее тип: текст, гипертекст, архивный файл, изображение, звук, или выяснить длину файла, или передать информацию на хост, то необходимо сначала методом openConnection () создать объект класса URLConnection или его подкласса HttpURLConnection.
После создания объекта соединение еще не установлено, и можно задать параметры связи. Это делается следующими методами:
setDoOutput (boolean out) — если аргумент out равен true, то передача пойдет от клиента на хост; значение по умолчанию false; setDoinput (boolean in) — если аргумент in равен true, то передача пойдет с хоста к клиенту; значение по умолчанию true, но если уже выполнено setDoOutput(true), то значение по умолчанию равно false; setUseCaches (boolean cache) — если аргумент cache равен false, то передача пойдет без кэширования, если true, то принимается режим по умолчанию; setDefaultUseCaches(boolean default) — если аргумент default равен true, то принимается режим кэширования, предусмотренный протоколом; setRequestProperty(String name, String value) — добавляет параметр name со значением value к заголовку посылаемого сообщения.После задания параметров нужно установить соединение методом connect (). После соединения задание параметров уже невозможно. Следует учесть, что некоторые методы доступа getxxxo, которым надо получить свои значения с хоста, автоматически устанавливают соединение, и обращение к методу connect () становится излишним.
Web-сервер возвращает информацию, запрошенную клиентом, вместе с заголовком, сведения из которого можно получить методами getxxxo, например:
getcontentType () — возвращает строку типа string, показывающую тип пересланной информации, например, "text/html", или null, если сервер его не указал; getcontentLength () — возвращает длину полученной информации в байтах или — 1, если сервер ее не указал; getcontent () — возвращает полученную информацию в виде объекта типа Object; getContentEncoding () — возвращает строку типа string с кодировкой полученной информации, или null, если сервер ее не указал.Два метода возвращают потоки ввода/вывода, созданные для данного соединения:
getlnputStream() — возвращает входной поток типа InputStream; getOutputStream() — возвращает выходной поток типа OutputStream.Прочие методы, а их около двадцати, возвращают различные параметры соединения.
Обращение к методу bhv.openstreamo, записанное в листинге 19.1, — это, на самом деле, сокращение записи
bhv.openConnection().getlnputStream()
В листинге 19.2 показано, как переслать строку текста по адресу URL.
Web-сервер, который получает эту строку, не знает, что делать с полученной информацией. Занести ее в файл? Но с каким именем, и есть ли у него право создавать файлы? Переслать на другую машину? Но куда?
Выход был найден в системе CGI (Common Gateway Interface), которая вкратце действует следующим образом. При посылке сообщения мы указываем URL исполнимого файла некоторой программы, размещенной на машине-сервере. Получив сообщение, Web-сервер запускает эту программу и передает сообщение на ее стандартный ввод. Вот программа-то и знает, что делать с полученным сообщением. Она обрабатывает сообщение и выводит результат обработки на свой стандартный вывод. Web-сервер подключается к стандартному выводу, принимает результат и отправляет его обратно клиенту.
CGI-программу можно написать на любом языке: С, C++, Pascal, Perl, PHP, лишь бы у нее был стандартный ввод и стандартный вывод. Можно написать ее и на Java, но в технологии Java есть более изящное решение этой задачи с помощью сервлетов (servlets). CGI-программы обычно лежат на сервере в каталоге cgi-bin.
Листинг 19.2. Посылка строки по адресу URL
import java.net.*;
import java.io.*;
class PostURL{
public static void main(String[] args){
String req = "This text is posting to URL";
try{
// Указываем URL нужной CGI-программы
URL url = new URL(" http://www.bhv.ru/cgi-bin/some.pl ");
// Создаем объект uc
URLConnection uc = url.openConnection();
// Собираемся отправлять
uc.setDoOutput(true);
// и получать сообщения
uc.setDoInput(true);
// без кэширования
uc.setUseCaches(false);
// Задаем тип
uc.setRequestProperty("content-type",
"application/octet-stream");
// и длину сообщения
uc.setRequestProperty("content-length", "" + req.length());
// Устанавливаем соединение
uc.connect();
// Открываем выходной поток
DataOutputStream dos = new DataOutputStream( uc.getOutputStreamO);
// и выводим в него сообщение, посылая его на адрес
URL dos.writeBytes(req);
// Закрываем выходной поток
dos.close();
// Открываем входной поток для ответа сервера
BufferedReader br = new BufferedReader(new InputStreamReader(
uc.getlnputStream() )) ;
String res = null;
// Читаем ответ сервера и выводим его на консоль
while ((res = br.readLine()) != null)
System.out.println(res);
br.close () ;
}catch(MalformedURLException me){
System.err.println(me);
}catch(UnknownHostException he){
System.err.println(he);
}catch(UnknownServiceException se){
System.err.println(se);
}catch(IOException ioe){
System.err.println(ioe);
}
}
}
Работа по протоколу TCP
Программы-серверы, прослушивающие свои порты, работают под управлением операционной системы. У машин-серверов могут быть самые разные операционные системы, особенности которых передаются программам-серверам.
Чтобы сгладить различия в реализациях разных серверов, между сервером и портом введен промежуточный программный слой, названный сокетом (socket). Английское слово socket переводится как электрический разъем, розетка. Так же как к розетке при помощи вилки можно подключить любой электрический прибор, лишь бы он был рассчитан на 220 В и 50 Гц, к соке-ту можно присоединить любой клиент, лишь бы он работал по тому же протоколу, что и сервер. Каждый сокет связан (bind) с одним портом, говорят, что сокет прослушивает (listen) порт. Соединение с помощью сокетов устанавливается так.
1. Сервер создает сокет, прослушивающий порт сервера.
2. Клиент тоже создает сокет, через который связывается с сервером, сервер начинает устанавливать (accept) связь с клиентом.
3. Устанавливая связь, сервер создает новый сокет, прослушивающий порт с другим, новым номером, и сообщает этот номер клиенту.
4. Клиент посылает запрос на сервер через порт с новым номером.
После этого соединение становится совершенно симметричным — два сокета обмениваются информацией, а сервер через старый сокет продолжает прослушивать прежний порт, ожидая следующего клиента.
В Java сокет — это объект класса socket из пакета java.io. В классе шесть конструкторов, в которые разными способами заносится адрес хоста и номер порта. Чаще всего применяется конструктор
Socket(String host, int port)
Многочисленные методы доступа устанавливают и получают параметры со-кета. Мы не будем углубляться в их изучение. Нам понадобятся только методы, создающие потоки ввода/вывода:
getlnputStream() — возвращает входной поток типа InputStream; getOutputStream() — возвращает выходной поток типа OutputStream.Приведем пример получения файла с сервера по максимально упрощенному протоколу HTTP.
1. Клиент посылает серверу запрос на получение файла строкой "POST filename HTTP/1.l\n\n", где filename — строка с путем к файлу на сервере.
2. Сервер анализирует строку, отыскивает файл с именем filename и возвращает его клиенту. Если имя файла filename заканчивается наклонной чертой /, то сервер понимает его как имя каталога и возвращает файл in-dex.html, находящийся в этом каталоге.
3. Перед содержимым файла сервер посылает строку вида "HTTP/1.1 code OK\n\n", где code — это код ответа, одно из чисел: 200 — запрос удовлетворен, файл посылается; 400 — запрос не понят; 404 — файл не найден.
4. Сервер закрывает сокет и продолжает слушать порт, ожидая следующего запроса.
5. Клиент выводит содержимое полученного файла в стандартный вывод System, out или выводит код сообщения сервера в стандартный вывод сообщений System, err.
6. Клиент закрывает сокет, завершая связь.
Этот протокол реализуется в клиентской программе листинга 19.3 и серверной программе листинга 19.4.
Листинг 19.3. Упрощенный HTTP-клиент
import java.net.*;
import java.io.*;
import java.util.*;
class Client{
public static void main(String[] args){
if (args.length != 3){
System.err.println("Usage: Client host port file");
System.exit(0) ;
}
String host = args[0];
int port = Integer.parselnt(args[1]);
String file = args[2];
try{
Socket sock = new Socket(host, port);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(
sock.getOutputStreamf)), true);
pw.println("POST " + file + " HTTP/1.l\n");
BufferedReader br = new BufferedReader(new InputStreamReader(
sock.getlnputStream() ) ) ;
String line = null;
line = br.readLine();
StringTokenizer st = new StringTokenizer(line);
String code = null;
if ((st.countTokens() >= 2) && st.nextToken().equals("POST")){
if ((code = st.nextToken()) != "200") {
System.err.println("File not found, code = " + code);
System.exit (0);
}
}
while ((line = br.readLine()) != null)
System.out.println{line);
sock.close();
}catch(Exception e){
System.err.println(e);
}
}
}
Закрытие потоков ввода/вывода вызывает закрытие сокета. Обратно, закрытие сокета закрывает и потоки.
Для создания сервера в пакете java.net есть класс serversocket. В конструкторе этого класса указывается номер порта
ServerSocket(int port)
Основной метод этого класса accept () ожидает поступления запроса. Когда запрос получен, метод устанавливает соединение с клиентом и возвращает объект класса socket, через который сервер будет обмениваться информацией с клиентом.
Листинг 19.4.
Упрощенный HTTP-сервер
import j ava.net.*;
import java.io.*;
import j ava.uti1.*;
class Server!
public static void main(String[] args){
try{
ServerSocket ss = new ServerSocket(Integer.parselnt(args[0]));
while (true)
new HttpConnect(ss.accept());
}catch(ArraylndexOutOfBoundsException ae){
System.err.println("Usage: Server port");
System.exit(0);
}catch(IOException e){
System.out.println(e);
}
}
}
class HttpConnect extends Thread{
private Socket sock;
HttpConnect(Socket s) {
sock = s;
setPriority(NORM_PRIORITY - 1);
start {) ;
}
public void run(){
try{
PrintWriter pw = new PrintWriter(new OutputStreamWriter(
sock.getOutputStream()}, true);
BufferedReader br = new BufferedReader(new InputStreamReader(
sock.getlnputStream() ) ) ;
String req = br.readLine();
System.out.println("Request: " + req);
StringTokenizer st = new StringTokenizer(req);
if ((st.countTokens() >= 2) && st.nextToken().equals("POST")){
if ((req = st.nextToken()).endsWith("/") II req.equals(""))
req += "index.html";
try{
File f = new File(req);
BufferedReader bfr =
new BufferedReader(new FileReader(f));
char[] data = new char[(int)f.length()];
bfr.read(data);
pw.println("HTTP/1.1 200 OK\n");
pw.write(data);
pw.flush();
}catch(FileNotFoundException fe){
pw.println("HTTP/1.1 404 Not FoundXn");
}catch(lOException ioe){
System.out.println(ioe);
}
}else pw.println("HTTP/l.l 400 Bad RequestW);
sock.close();
}catch(IOException e){
System.out.println(e);
}
}
}
Вначале следует запустить сервер, указав номер порта, например:
Java Server 8080
Затем надо запустить клиент, указав IP-адрес или доменное имя хоста, номер порта и имя файла:
Java Client localhost 8080 Server.Java
Сервер отыскивает файл Server.java в своем текущем каталоге и посылает его клиенту. Клиент выводит содержимое этого класса в стандартный вывод и завершает работу. Сервер продолжает работать, ожидая следующего запроса.
Замечание по отладке
Программы, реализующие стек протоколов TCP/IP, всегда создают так называемую "петлю" с адресом 127.0.0.1 и доменным именем localhost. Это адрес самого компьютера. Он используется для отладки приложений клиент-сервер. Вы можете запускать клиент и сервер на одной машине, пользуясь этим адресом.
Работа по протоколу UDP
Для посылки дейтаграмм отправитель и получатель создают сокеты дейта-граммного типа. В Java их представляет класс DatagramSocket. В классе три конструктора:
DatagramSocket () — создаваемый сокет присоединяется к любому свободному порту на локальной машине; DatagramSocket (int port) — создаваемый сокет присоединяется к порту port на локальной машине; DatagramSocket(int port, InetAddress addr) — создаваемый СОКСТ при соединяется к порту port; аргумент addr — один из адресов локальной машины.Класс содержит массу методов доступа к параметрам сокета и, кроме того, методы отправки и приема дейтаграмм:
send(DatagramPacket pack) — отправляет дейтаграмму, упакованную в пакет pack; receive (DatagramPacket pack) — дожидается получения дейтаграммы и заносит ее в пакет pack.При обмене дейтаграммами соединение обычно не устанавливается, дейтаграммы посылаются наудачу, в расчете на то, что получатель ожидает их. Но можно установить соединение методом
connect(InetAddress addr, int port)
При этом устанавливается только одностороннее соединение с хостом по адресу addr и номером порта port — или на отправку или на прием дейтаграмм. Потом соединение можно разорвать методом
disconnect()
При посылке дейтаграммы по протоколу JJDP сначала создается сообщение в виде массива байтов, например,
String mes = "This is the sending message.";
byte[] data = mes.getBytes();
Потом записывается адрес — объект класса inetAddress, например:
InetAddress addr = InetAddress.getByName (host);
Затем сообщение упаковывается в пакет — объект класса DatagramPacket. При этом указывается массив данных, его длина, адрес и номер порта:
DatagramPacket pack = new DatagramPacket(data, data.length, addr, port)
Далее создается дейтаграммный сокет
DatagramSocket ds = new DatagramSocket()
и дейтаграмма отправляется
ds.send(pack)
После посылки всех дейтаграмм сокет закрывается, не дожидаясь какой-либо реакции со стороны получателя:
ds.close ()
Прием и распаковка дейтаграмм производится в обратном порядке, вместо метода send () применяется метод receive (DatagramPacket pack).
В листинге 19.5 показан пример класса Sender, посылающего сообщения, набираемые в командной строке, на localhost, порт номер 1050. Класс Recipient, описанный в листинге 19.6, принимает эти сообщения и выводит их в свой стандартный вывод.
Листинг 19.5. Посылка дейтаграмм по протоколу UDP
import java.net.*;
import java.io.*;
class Sender{
private String host;
private int port;
Sender(String host, int port){
this.host = host;
this.port = port;
}
private void sendMessage(String mes){
try{
byte[] data = mes.getBytes();
InetAddress addr = InetAddress.getByName(host);
DatagramPacket pack =
new DatagramPacket(data, data.length, addr, port);
DatagramSocket ds = new DatagramSocket();
ds.send(pack);
ds.close();
}catch(IOException e){
System.err.println(e);
}
}
public static void main(String[] args){
Sender sndr = new Sender("localhost", 1050);
for (int k = 0; k < args.length; k++)
sndr.sendMessage(args[k]);
}
}
Листинг 19.6. Прием дейтаграмм по протоколу UDP
import j ava.net.*;
import java.io.*;
class Recipient{
public static void main(String[] args)(
try{
DatagramSocket ds = new DatagramSocket(1050);
while (true){
DatagramPacket pack =
new DatagramPacket(new byte[1024], 1024);
ds.receive(pack);
System.out.println(new String(pack.getData()));
}
)catch(Exception e){
System.out.println(e);
}
}
}