Java. HTTP протокол и работа с WEB

Автор: content Понедельник, Апрель 9th, 2012 Нет комментариев

Рубрика: Язык Java

Вступление

Цель данной статьи — дать понимание внутренней структуры интернета, работы механихмов, составляющих эту сеть. Так же рассказывается о том, как пишутся и используются программные модули, ориентированные на построение WWW и научить работе с HTTP протоколом, используя средства Java. Предполагается, что читающий данную статью уже знает язык программирования Java, но некоторые вещи объяснены с самого начала.

Работа с TCP/IP в Java. Сокеты

Итак, для начала немного теории. HTTP (Hyper Text Transfert Protocol) был изначально создан для пересылки HTML документов, отсюда и «заточка» этого протокола под работу с отдельными документами, преимущественно текстовыми. HTTP в своей работе использует возможности TCP/IP, поэтому рассмотрим возможности, предоставляемые java для работы с последним.

В джаве для этого существует специальный пакет «java.net», содержащий класс java.net.Socket. Socket в переводе означает «гнездо», название это было дано по аналогии с гнёздами на аппаратуре, теми самыми, куда подключают штепсели. Соответственно этой аналогии, можно связать два «гнезда», и передавать между ними данные. Каждое гнездо принадлежит определённому хосту (Host — хозяин, держатель). Каждый хост имеет уникальный IP (Internet Packet) адрес. На данный момент интернет работает по протоколу IPv4, где IP адрес записывается 4 числами от 0 до 255 — например, 127.0.0.1 (подробнее о распределении IP адресов тут — RFC 790, RFC 1918, RFC 2365, о версии IPv6 читайте тут — RFC 2373)

Гнёзда монтируются на порт хоста (port). Порт обозначается числом от 0 до 65535 и логически обозначает место, куда можно пристыковать (bind) сокет. Если порт на этом хосте уже занят каким-то сокетом, то ещё один сокет туда пристыковать уже не получится. Таким образом, после того, как сокет установлен, он имеет вполне определённый адрес, символически записывающийся так [host]:[port], к примеру — 127.0.0.1:8888 (означает, что сокет занимает порт 8888 на хосте 127.0.0.1)

 

TCP/IP: логическая структура соединений через сокеты
TCP/IP: логическая структура соединений через сокетыДля того, чтобы облегчить жизнь, чтобы не использовать неудобозапоминаемый IP адрес, была придумана система DNS (DNS — Domain Name Service). Цель этой системы — сопоставлять IP адресам символьные имена. К примеру, адресу «127.0.0.1″ в большинстве компьютеров сопоставленно имя «localhost» (в просторечье — «локалхост»).

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

Клиентский сокет

Итак, вернёмся к классу java.net.Socket Наиболее удобно инициализировать его следующим образом:

public Socket(String host, int port) throws UnknownHostException, IOException

В строковой константе host можно указать как IP адрес сервера, так и его DNS имя. При этом программа автоматически выберет свободный порт на локальном компьютере и «привинтит» туда ваш сокет, после чего будет предпринята попытка связаться с другим сокетом, адрес которого указан в параметрах инициализации. При этом могут возникнуть два вида исключений: неизвестный адрес хоста — когда в сети нет компьютера с таким именем или ошибка отсутствия связи с этим сокетом.

Так же полезно знать функцию

public void setSoTimeout(int timeout) throws SocketException

Эта функция устанавливает время ожидания (timeout) для работы с сокетом. Если в течение этого времени никаких действий с сокетом не произведено (имеется ввиду получение и отправка данных), то он самоликвидируется. Время задаётся в секундах, при установке timeout равным 0 сокет становится «вечным».

Для некоторых сетей изменение timeout невозможно или установлено в определённых интервалах (к примеру от 20 до 100 секунд). При попытке установить недопустимый timeout, будет выдано соответственное исключение.

Программа, которая открывает сокет этого типа, будет считаться клиентом, а программа-владелец сокета, к которому вы пытаетесь подключиться, далее будет называться сервером. Фактически, по аналогии гнездо-штепсель, программа-сервер — это и будет гнездо, а клиент как раз является тем самым штепселем.

Сокет сервера

Как установить соединение от клиента к серверу я только что описал, теперь — как сделать сокет, который будет обслуживать сервер. Для этого в джава существует следующий класс: java.net.ServerSocket Наиболее удобным инициализатором для него является следующий:

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

Как видно, в качестве третьего параметра используется объект ещё одного класса — java.net.InetAddress Этот класс обеспечивает работу с DNS и IP именами, по этому вышеприведённый инициализатор в программах можно использовать так:

ServerSocket(port, 0, InetAddress.getByName(host)) throws IOException

Для этого типа сокета порт установки указывается прямо, поэтому, при инициализации, может возникнуть исключение, говорящее о том, что данный порт уже используется либо запрещён к использованию политикой безопасности компьютера.

После установки сокета, вызывается функция

public Socket accept() throws IOException

Эта функция погружает программу в ожидание того момента, когда клиент будет присоединяться к сокету сервера. Как только соединение установлено, функция возвратит объект класса Socket для общения с клиентом.

Клиент-сервер через сокеты. Пример

Как пример — простейшая программа, реализующая работу с сокетами.

Со стороны клиента программа работает следующим образом: клиент подсоединяется к серверу, отправляет данные, после чего получает данные от сервера и выводит их.

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

 

логическая структура работы программ-примеров
Логическая структура работы программ-примеровПрограмма простого TCP/IP клиента
(SampleClient.java)

import java.io.*;
import java.net.*;

class SampleClient extends Thread
{
public static void main(String args[])
{
try
{
// открываем сокет и коннектимся к localhost:3128
// получаем сокет сервера
Socket s = new Socket(«localhost», 3128);

// берём поток вывода и выводим туда первый аргумент
// заданный при вызове, адрес открытого сокета и его порт
args[0] = args[0]+»\n»+s.getInetAddress().getHostAddress()
+»:»+s.getLocalPort();
s.getOutputStream().write(args[0].getBytes());

// читаем ответ
byte buf[] = new byte[64*1024];
int r = s.getInputStream().read(buf);
String data = new String(buf, 0, r);

// выводим ответ в консоль
System.out.println(data);
}
catch(Exception e)
{System.out.println(«init error: «+e);} // вывод исключений
}
}

Программа простого TCP/IP сервера
(SampleServer.java)

import java.io.*;
import java.net.*;

class SampleServer extends Thread
{
Socket s;
int num;

public static void main(String args[])
{
try
{
int i = 0; // счётчик подключений

// привинтить сокет на локалхост, порт 3128
ServerSocket server = new ServerSocket(3128, 0,
InetAddress.getByName(«localhost»));

System.out.println(«server is started»);

// слушаем порт
while(true)
{
// ждём нового подключения, после чего запускаем обработку клиента
// в новый вычислительный поток и увеличиваем счётчик на единичку
new SampleServer(i, server.accept());
i++;
}
}
catch(Exception e)
{System.out.println(«init error: «+e);} // вывод исключений
}

public SampleServer(int num, Socket s)
{
// копируем данные
this.num = num;
this.s = s;

// и запускаем новый вычислительный поток (см. ф-ю run())
setDaemon(true);
setPriority(NORM_PRIORITY);
start();
}

public void run()
{
try
{
// из сокета клиента берём поток входящих данных
InputStream is = s.getInputStream();
// и оттуда же — поток данных от сервера к клиенту
OutputStream os = s.getOutputStream();

// буффер данных в 64 килобайта
byte buf[] = new byte[64*1024];
// читаем 64кб от клиента, результат — кол-во реально принятых данных
int r = is.read(buf);

// создаём строку, содержащую полученную от клиента информацию
String data = new String(buf, 0, r);

// добавляем данные об адресе сокета:
data = «»+num+»: «+»\n»+data;

// выводим данные:
os.write(data.getBytes());

// завершаем соединение
s.close();
}
catch(Exception e)
{System.out.println(«init error: «+e);} // вывод исключений
}
}

После компиляции, получаем файлы SampleServer.class и SampleClient.class (все программы здесь и далее откомпилированы с помощью JDK v1.4) и запускаем вначале сервер:

java SampleServer

а потом, дождавшись надписи «server is started», и любое количество клиентов:

java SampleClient test1
java SampleClient test2

java SampleClient testN

Если во время запуска программы-сервера, вместо строки «server is started» выдало строку типа

init error: java.net.BindException: Address already in use: JVM_Bind

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

Заметки

Отметим немаловажную особенность сокета сервера: он может принимать подключения сразу от нескольких клиентов одновременно. Теоретически, количество одновременных подключений неограниченно, но практически всё упирается в мощность компьютеров. Кстати, эта проблема конечной мощности компьютеров используется в DOS атаках на серверы: их просто закидывают таким количеством подключений, что компьютеры не справляются с нагрузкой и «падают».

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

Стоит упомянуть, что абстракцию Socket — ServerSocket и работу с потоками данных используют C/C++, Perl, Python, многие другие языки программирования и API операционных систем, так что многое из сказанного подходит к применению не только для платформы Java.

Протокол HTTP

Протокол HTTP работает поверх TCP/IP. Фактически же это означает, что клиент открывает сокет до сервера, пишет туда HTTP запрос (request), сервер читает запрос, обрабатывает его и посылает результат обработки (response) обратно клиенту.

Любой HTTP запрос, как и любой ответ по этому протоколу состоит из двух блоков: заголовок и собственно данные. Заголовок отделён от данных двойным символом переноса строки (в Java это будет «\n\n», хотя допускается и «\r\n\r\n» для платформы Windows).

Так как HTTP был изначально ориентирован на пересылку прежде всего текстовой информации, то HTTP заголовок является полностью текстовым, все символы, передающиеся в нём, являются печатными (прежде всего цифры и литеры латинского алфавита A-Z, a-z, а также набор других отображаемых символов + символ переноса строки «\n» или «\r\n»). При передаче в HTTP заголовке других символов, будет выдана ошибка «400 Вad request».

HTTP запросы. CGI интерфейс. Методы

Разберём подробнее HTTP запрос клиента. Он может выглядеть например так:

POST http://localhost/ HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Accept-Language: ru
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: localhost
Proxy-Connection: Keep-Alive

param1=1&param2=2

Взглянув на пример, можно заметить, что запрос начинается со слова «POST». Это слово означает метод передачи данных на сервер, в котором дополнительные данные запроса (строка «param1=1&param2=2″) передаются после заголовка.

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

<form action="http://localhost/" method="post">
<input type=hidden name="param1" value="1">
<input type=hidden name="param2" value="2">
<input type=submit></form>

Как видно из примера, параметры записываются в виде

[имя параметра 1]=[значения параметра 1]&[имя параметра 2]=[значения параметра 2]&...

Такой вид записи является стандартным и носит название CGI интерфейса (Common Gateway Interface — базовый интерфейс гейтов (gate — врата, ещё одно название серверов)). Все данные, отсылаемые браузером, обработавшим HTML к серверу записываются именно в таком формате. При этом символы, отличные от печатных ANSI, записываются в формате %NN, где NN — это шестнадцатиричный код символа. К примеру, пробел будет записан как %20, а символ % — как %25 (см. ASCII & ANSI Character Codes). Так как русские кириллические символы не входят в набор печатных ANSI символов, то в HTTP заголовках они тоже заменяются подобным образом.

Наиболее часто употребим ещё один метод запроса — «GET». Фактически все запросы, не требующие отправки данных — например запрос страницы, производятся этим способом. Впрочем, данные можно отправлять и GET методом — изменим форму запроса:

<form action="http://localhost/" method="get">
<input type=hidden name="param1" value="1">
<input type=hidden name="param2" value="2">
<input type=submit></form>

и получим следующий HTTP запрос:

GET http://localhost/?param1=1&param2=2 HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Accept-Language: ru
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: localhost
Proxy-Connection: Keep-Alive

Как видно, строка «param1=1&param2=2″ переместилась выше и добавилась к строке «http://localhost/» после знака «?». Так же изменилось первое слово в HTTP заголовке, остальное осталось без изменения.

Достоинством метода GET является то, что в строке браузера видно, какие данные были отправлены. К недостаткам же относится то, что длина отправляемых данных таким способом (в отличие от метода POST) ограничена — некоторые серверы, как и некоторые браузеры, имеют лимит на длину адреса запрашиваемого документа. Соответственно адрес с длинной строкой запроса может быть либо обрезан, либо сервер возвратит ошибку «414 Request-URI Too Long».

HTTP запрос

Разберём по строкам HTTP заголовок запроса:

Первая строка, первое слово — имя метода запроса. Это слово может быть одно из следующих:

OPTIONS
GET
HEAD
POST
PUT
DELETE
TRACE
CONNECT

В данной статье я буду касаться прежде всего двух методов — GET и POST, подробнее об остальных читайте тут — RFC 2616, Section 5. Вкратце лишь опишу действия остальных:

Метод HEAD по действию практически идентичен методу GET с одним отличием — в ответе на метод HEAD сервер выдаёт только HTTP заголовок, не выдавая содержимого документа.

Метод PUT по действию идентичен методу POST, но как и HEAD выдаёт только заголовок HTTP.

Метод OPTIONS выдаёт все действия, которые можно совершить с документом.

Метод DELETE указывает серверу, чтобы он предпринял попытку к удалению документа. Возможным ответом является ошибка политики безопасности («403 Forbidden»).

Методом TRACE можно получить путь запроса до сервера, список узловых точек, гейтов (Gate), путь через прокси-сервера.

Метод CONNECT возвращает есть ли связь с сервером и поддерживает ли сервер HTTP протокол.

Сразу после ключегого слова, определяющего метод, идёт символ пробела и указан URI документа запрашиваемого с сервера. После URI документа идёт ещё один символ пробела и название протокола (строка «HTTP/1.1″).

Что такое URI? URI расшифровывается как Uniform Resource Identifier (формат записи индитефикатора ресурса), полностью он описан тут — RFC 2396, а нас интересует лишь то, как с помощью его записывается адрес документа. Для HTTP существует разновидность стандарта URI, называемая URL (Uniform Resource Location — формат записи нахождения ресурса), к примеру

http://devresource.org:80/javalinks/catalog.php3?name=java&cat=2#section1

Из приведённого примера URL можно выделить логические части:

1) [http://]
2) [devresource.org:80/javalinks/catalog.php3]
3) [?name=java&cat=2]
4) [#section1]

часть (1) указывает на протокол доступа к документу, часть (2) — можно разбить на две части -

[devresource.org]
[:80]
[/javalinks/catalog.php3]

это имя хоста (вместо имени devresource.org может стоять и IP адрес), порт сервера через символ «:» и путь (path) до документа от корня (root) сервера. Стандартным портом для HTTP сервера является порт 80 и, поэтому, его можно не указывать. Внимание! Если порт сервера отличается от 80, то в URL его нужно обязательноуказать.

Третья часть URL — это GET часть запроса, отделена от документа символом «?».

И, наконец, последняя часть URI — «секция», отделённая символом «#». При HTML форматировании в документа можно положить закладку (по другому — установить якорь, он же anchor, отсюда и название HTML тега <A>). Если в URI ресурса указана секция, то в HTML ищется одноимённая закладка, а браузер при отображении документа показывает текст, отмеченный якорем.

К примеру, для HTML документа sample.html

...
test 24
<a name="section1">test 25</a>
test 26
<a name="section2">test 27</a>
test 28
...

при вызове sample.html#section1 документ будет проскролирован до закладки «section1″, а при указании sample.html#section2 — будет показано место, помеченное в документе, как «section2″.

Части URL с номерами (3) и (4) являются необязательными. Если нет необходимости, их можно не указывать. В первой строке HTTP запроса так же можно указать не полный URL, а лишь путь до документа — к примеру так:

GET /#section1 HTTP/1.1

или так:

GET /javalinks/catalog.php3?name=java&cat=2#section1 HTTP/1.1

В таких случаях имя хоста берётся из параметра HTTP запроса «Host».

Внимание! Имя протокола в URI в первой строке заголовка сигнализирует серверу о том, что путь до документа указан вместе с именем хоста. То есть, если послать серверу заголовок, начинающийся со строки

GET localhost/?param1=1&param2=2 HTTP/1.1

то сервер будет искать документ

http://localhost/localhost/?param1=1&param2=2

а не

http://localhost/?param1=1&param2=2

Как и каждая строка HTTP заголовка, первая строка запроса заканчивается символом переноса строки («\n»).

Параметры HTTP запроса

Далее приведены основные параметры для HTTP заголовка. Каждая строка, содержащая параметр начинается с ключевого слова (например «Host»), потом идёт символ двоеточие, пробел, значение параметра и символ переноса строки. Приведённые параметры соответствуют стандарту RFC 2616 (HTTP/1.1). Здесь приводится не весь список возможных полей запроса и их значений.

Host: localhost

Этот параметр содержит имя хоста, например «localhost» или «localhost:80″ (если порт 80, то его можно не указывать, если порт отличается от 80, то его нужно обязательно указать). Это второй обязательный параметр HTTP заголовка (первый — HTTP метод и имя протокола).

Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*

В этом параметре через запятую указаны MIME типы документов, которые способен обработать браузер. Так же в MIME типе указываются доступные к обработке кодировки документов. Подробнее о стандарте MIME смотрите тут — RFC 2045. Символ «*» в указании типа означает, что браузер может обработать весь класс документов. К примеру, image/* означает, что браузер может обработать и image/gif, и image/x-xbitmap, и image/jpeg, и image/pjpeg и вообще любые документы изображений, а заключительный тип — */* указывает, что браузер обработает любые документы, присланные сервером. На деле это обычно означает, что если MIME тип присланного сервером документа браузеру неизвестен, то он предложит сохранить его на диск.

Accept-Language: ru, en
Accept-Charset: windows-1251, KOI8-R

Эти параметры отвечают за языки. В первом — через запятую указываются предпочтительные языки для сервера. В частности Google.com, обработав этот параметр, перенаправит вас на русскоязычную страничку. Во втором — кодировка, в которой закодированы символы в CGI запросе. Также через запятую могут быть указаны предпочитаемые кодировки для ответа сервера.

Accept-Encoding: compressed, gzip

Тут указаны возможные варианты пересылки данных. В частности, я привёл в пример запроса, показывающего, что браузер готов принимать HTML документ в сжатом виде.

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; MyIE2; .NET CLR 1.0.3705)

Имя HTTP клиента. Многие браузеры тут же указывают операционную систему, плагины и прочие нашлёпки.

Referer: http://localhost/?test=test

Очень полезный параметр. Значением этого поля является URL ресурса, с которой был осуществлён переход. Фактически, когда вы нажимаете на ссылку в HTML документе, скорее всего адрес этого документа будет записан в этот параметр.

Cookie: param1=value1; param2=value2

В этом параметре браузер отправляет cookie (или просто куки) — данные, записанные сервером на компьютер клиента. Как видно, куки отправляются не с помощью CGI интерфейса, их форматирование отличается:

[имя параметра1]=[значение параметра1]; [имя параметра2]=[значение параметра2]; ...

Впрочем, значения параметров кодируются точно так же, как и в CGI — «неправильные» символы заменяются с помощью %NN. Подробнее о куках читайте тут — Cookie Specification.

Range-Unit: 2015 | 1024

Очень полезный параметр, позволяющий получить с сервера не весь документ, а только его часть. Именно этот параметр используют менеджеры докачек типа flashget. В данном примере указано, что клиент хочет получить кусок документа, начиная с 2015 байта и длиной в 1 килобайт. Если сервер поддерживает докачку и документ не является динамическим, то будет выдана запрашиваемая часть. В противном случае сервер вернёт ошибку о том, что действие не поддерживается или начнёт выдавать документ полностью.

Pragma: no-cache
Cache-Control: no-cache, must-revalidate

Параметры, указывающие серверу, что этот документ не надо брать из кэша. Другие варианты значений могут быть:
«public» — документ является публичным, его может брать любой клиент из кэша
«private» — документ является приватным, только для данного клиента
«no-store» — не сохранять в кэш
«no-transform» — не модифицировать документ, уже содержащийся в кэше
«must-revalidate» — обязан обновить документ, лежащий в кэше (и браузер и прокси)
«proxy-revalidate» — в кэше должен обновить только прокси сервер
«max-age=[seconds]» — сохраняет в кэш на количество секунд, указанных в параметре, начиная со времени сохранения; по истечению этого времени, документ удаляется
Допустимо указание параметра в следующем виде:

Pragma: must-revalidate, max-age=1000

но некоторые комбинации значений могут вызвать ошибку «400 Bad request»

Proxy-Connection: Keep-Alive

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

Proxy-Connection: close

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

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

Отправка файла методом POST

Отдельно стоит рассказать об отправке файлов с помощью метода POST. Для того, чтобы отправить файл этим методом, нужно в форме отправки сообщений указать специальный параметр «enctype=’multipart/form-data’»:

<form action='http://localhost' method=post enctype='multipart/form-data'>
<input type='hidden' name='test' value='test'>
<input type='file' name='testfile'>
<input type='submit' value='send'></form>

Допустим, мы отправляем файл «c:\test.txt» размера 14 байт, содержащий текст «This a test!!!». В этом случае данные будут отправлены следующим образом:

POST http://localhost/ HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------------7d33188e01e4
Host: localhost
Content-Length: 254

-----------------------------7d33188e01e4
Content-Disposition: form-data; name="test"

test
-----------------------------7d33188e01e4
Content-Disposition: form-data; name="testfile"; filename="c:\test.txt"
Content-Type: text/plain

This a test!!!

Как видно из примера, добавляется ещё два HTTP параметра -

Content-Type: multipart/form-data; boundary=---------------------------7d33188e01e4

Эта строка говорит, что все отсылаемые данные будут передаваться по частям, а делителем этих частей будет выступать строка «——————————7d33188e01e4″ и перенос строки после неё. Вообще-то, делителем может выступать совершенно любой набор символов, лишь бы подобного не было в передаваемых данных.

Content-Length: 342

Этот параметр сообщает серверу количество данных, содержащихся после HTTP заголовка.

Ну и сами данные передаются с помощью HTTP-subheader (субзаголовок HTTP)

Content-Disposition: form-data; name="test"

Тут указывается название переменной, после чего идут два символа переноса строки и сами данные. Конец данных означает либо символ переноса строки и делитель (boundary), либо конец полученых данных.

Другой субзаголовок -

Content-Type: text/plain

Он передаёт серверу MIME тип отправляемого файла. Нужно заметить, что при передаче данных таким способом, непечатные символы не заменяются на %NN, а отправляются как есть.

HTTP ответ

Перейдём к ответу сервера. Вот пример ответа сервера клиенту (сервер выдаёт текстовый файл, содержащий строку «This a test!!!»):

HTTP/1.1 200 OK
Date: Mon, 07 Apr 2003 14:40:25 GMT
Server: Apache/1.3.20 (Win32) PHP/4.3.0
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/plane

This a test!!!

Ещё один пример, сервер выдаёт файл test.zip:

HTTP/1.1 200 OK
Date: Mon, 07 Apr 2003 14:51:19 GMT
Server: Apache/1.3.20 (Win32) PHP/4.3.0
Last-Modified: Mon, 07 Apr 2003 14:51:00 GMT
Accept-Ranges: bytes
Content-Length: 673
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: application/zip
Content-Disposition: attachment; filename=test.zip
Pragma: no-cache

....(содержимое zip файла)

Тут мы тоже видим HTTP заголовок, отделённый от тела документа двумя символами переноса строки.

Разберём заголовок. Он начинается с названия протокола «HTTP/1.1″, после чего идёт пробел, затем — код возврата «200 OK». После кода возврата идёт символ переноса строки.

Коды ответов сервера

Вот основные коды возврата, определённые для серверов:

Коды с номером типа 1xx: информационный — запрос послан, идёт процесс:

  • «101 Switching Protocols» — переключение протокола

Коды с номером типа 2xx: удачное завершение — запрос полностью послан, прочитан/понят сервером и принят им:

  • «200 OK» — запрос успешно получен, понят, принят и выполнен
  • «201 Created» — создано
  • «202 Accepted» — принято
  • «203 Non-Authoritative Information» — нерабочая информация
  • «204 No Content» — нет информации к ответу
  • «205 Reset Content» — очистка ответа
  • «206 Partial Content» — выдаётся запрошенная часть документа (см. «Range-Unit» в запросе клиента)

Коды с номером типа 3xx: перенаправление — действие нуждается в уточнении либо просто информационный ответ:

  • «300 Multiple Choices» — множественный выбор — по данному запросу обнаружено несколько вариантов документов
  • «301 Moved Permanently» — документ переехал
  • «302 Found» — найдено
  • «303 See Other» — смотри остальные
  • «304 Not Modified» — не изменён
  • «305 Use Proxy» — используй прокси
  • «307 Section» — временное перемещение запроса

Коды с номером типа 4xx: ошибка клиента — запрос клиента имеет либо неправильный синтаксис, либо не понят:

  • «400 Bad Request» — плохой запрос
  • «401 Unauthorized» — нет авторизации
  • «402 Payment Required» — коммерческий ресурс, у вас нет денег на счету
  • «403 Forbidden» — запрещение доступа к ресурсу (политика безопасности)
  • «404 Not Found» — ресурс не найден
  • «405 Method Not Allowed» — метод не поддерживается
  • «406 Not Acceptable» — нет доступа к хосту
  • «407 Proxy Authentication Required» — для работы с прокси вы должны авторизоваться
  • «408 Request Time-out» — слишком долго не было данных с сервера (связь плохая или сервер упал)
  • «409 Conflict» — конфликт
  • «410 Gone» — процесс идёт (не мешайте)
  • «411 Length Required» — требуется длина посылаемых данных
  • «412 Precondition Failed» — неправильные умолчания
  • «413 Request Entity Too Large» — содержимое запроса слишком велико для этого сервера
  • «414 Request-URI Too Large» — слишком длинный адрес запрашиваемого ресурса
  • «415 Unsupported Media Type» — в «Accept» не указан поддерживаемый сервером формат данных
  • «416 Requested range not satisfiable» — требуемый кусок (с помощью «Range-Unit») имеет неверные размеры
  • «417 Expectation Failed» — неожиданная ошибка при разборе запроса (может возникнуть при пересылке типа «multipart/form-data» при неправильном делителе)

Коды с номером типа 5xx: ошибка сервера — сервер не может обработать запрос клиента

  • «500 Internal Server Error» — внутренняя ошибка сервера
  • «501 Not Implemented» — не применяется (этот запрос не применим)
  • «502 Bad Gateway» — «плохие врата» — сервер не обрабатывает запросы с этого сегмента IP
  • «503 Service Unavailable» — такой сервис недоступен (к примеру TRACE запрос)
  • «504 Gateway Time-out» — слишком долго сервер пытался получить данные, связь плохая
  • «505 HTTP Version not supported» — версия HTTP, указанная в запросе, не поддерживается данным сервером

Параметры HTTP ответа

Продолжим разбор параметров заголовка в ответе сервера. Прежде всего упомяну, что параметры «Cache-Control», «Pragma» и «Proxy-Connection» идентичны как для запроса, так и для ответа, по этому всё сказанное про них выше, применимо и тут.

Set-Cookie: name=value; expires=date; path=PATH; domain=HOSTNAME; secure

Не буду подробно останавливаться на этом параметре. Он устанавливает или удаляет cookie и подробно о нём написано в Cookie Specification.

Location: http://www.devresource.org

Данный параметр указывает браузеру, что нужно открыть ресурс http://www.devresource.org вместо текущего. В значении этого параметра указывается URI ресурса для перехода.

Date: Mon, 07 Apr 2003 14:51:19 GMT

Параметр показывает дату документа. Это либо текущая дата (если документ динамический), либо дата создания отправляемого файла. Дата представлена в формате GMT.

Last-Modified: Mon, 07 Apr 2003 14:51:00 GMT

Параметр показывает дату последнего изменения документа.

Server: Apache/1.3.20 (Win32) PHP/4.3.0

Параметр содержит имя сервера.

Keep-Alive: timeout=15, max=100
Connection: Keep-Alive

Эти два параметра сообщают, что поддерживается постоянное соединение с сервером (вы противном случае было бы «Connection: close»), что текущее время timeout для сокета сервера составляет 15 секунд и что клиент может изменить это время максимум до 100 секунд.

Accept-Ranges: bytes

Этот параметр существует, чтобы указать клиенту, какая часть документа ему пересылается (в случае присутствия «Range-Unit» в запросе) Параметр этот может содержать значение «bytes», означающее, что пересылается файл целиком. Так же «none» (или этот параметр может быть просто опущен), означающее, что докачка не используется или не поддерживается, а строка «Accept-Ranges: 1:637″ будет означать, что пересылается кусок документа с байта под номером 1 и длиной в 637 байт.

Content-Length: 673

Длина пересылаемого документа.

Content-Type: application/zip

MIME тип пересылаемого документа

Content-Disposition: attachment; filename=test.zip

указывает, что пересылаемый файл имеет название «test.zip»

Accept-Charset: windows-1251

указывает кодировку текста документа (в данном случае — русскую кодировку windows)

Accept-Encoding: compress, gzip

этот параметр используется сервером, чтобы указать клиенту, что документ ему передаётся в сжатом виде (и для сжатия используется стандарт gzip)

Accept-Language: ru

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

Transfer-Encoding: chunked

Данный параметр показывает метод выдачи данных сервером. В данном случае сервер будет выдавать данные по кусочкам, а не всё сразу.

На этом краткое описание HTTP протокола можно считать завершённым.

Программы для работы с WEB

Для того, чтобы получше понять как работать с HTTP, эта часть статьи будет посвящена написанию простой, но полезной утилиты для иследования WEB узлов, рассмотрения работы proxy-серверов, написанию простого кэширующего proxy и простейшего WEB сервера.

Простой HTTP клиент

Начну же c написания простого HTTP клиента. Идея этой утилиты проста. Иногда, для исследования защиты сервера, очень удобно подсмотреть какой заголовок он выдаёт браузеру и какие куки он при этом присылает клиенту. Также эта утилита пригодиться, если требуется подменить поле «User-Agent» или послать такие cookie, которые сервер не записывал на ваш компьютер. Программа выполняет одно не очень хитрое действие: она читает заранее приготовленный файл, содержащий HTTP запрос, и отсылает его серверу, а ответ сервера, вместе с HTTP заголовком, записывает в другой файл, имя которого задано в параметрах инициализации.

HTTPClient.java

import java.io.*;
import java.net.*;

class HTTPClient
{
// первый аргумент — имя файла, содержащего HTTP запрос
// предполагается, что запрос не будет больше 64 килобайт
// второй — имя файла, куда будет слит ответ сервера
public static void main(String args[])
{
try
{
byte buf[] = new byte[64*1024];
int r;

// читаем файл с запросом в переменную header
FileInputStream fis = new FileInputStream(args[0]);
r = fis.read(buf);
String header = new String(buf, 0, r);
fis.close();

// выделяем из строки запроса хост, порт и URL ресурса
// для выделения используется специальнонаписанная ф-ия extract
String host = extract(header, «Host:», «\n»);

// если не найден параметр Host — ошибка
if(host == null)
{
System.out.println(«invalid request:\n»+header);
return;
}

// находим порт сервера, по умолчанию он — 80
int port = host.indexOf(«:»,0);
if(port < 0) port = 80;
else
{
port = Integer.parseInt(host.substring(port+1));
host = host.substring(0, port);
}

// открываем сокет до сервера
Socket s = new Socket(host, port);

// пишем туда HTTP request
s.getOutputStream().write(header.getBytes());

// получаем поток данных от сервера
InputStream is = s.getInputStream();

// Открываем для записи файл, куда будет слит лог
FileOutputStream fos = new FileOutputStream(args[1]);

// читаем ответ сервера, одновременно сливая его в открытый файл
r = 1;
while(r > 0)
{
r = is.read(buf);
if(r > 0)
fos.write(buf, 0, r);
}

// закрываем файл
fos.close();
s.close();
}
catch(Exception e)
{e.printStackTrace();} // вывод исключений
}

// «вырезает» из строки str часть, находящуюся между строками start и end
// если строки end нет, то берётся строка после start
// если кусок не найден, возвращается null
// для поиска берётся строка до «\n\n» или «\r\n\r\n», если таковые присутствуют
protected static String extract(String str, String start, String end)
{
int s = str.indexOf(«\n\n», 0), e;
if(s < 0) s = str.indexOf(«\r\n\r\n», 0);
if(s > 0) str = str.substring(0, s);
s = str.indexOf(start, 0)+start.length();
if(s < start.length()) return null;
e = str.indexOf(end, s);
if(e < 0) e = str.length();
return (str.substring(s, e)).trim();
}
}

Компилируем программу, получаем HTTPClient.class и попробуем её использовать: заготовим файл с HTTP заголовком

GET http://www.devresource.org HTTP/1.1
Host: www.devresource.org
User-Agent: HTTPClient

(не забудьте два переноса строки в конце файла) и сохраним его, скажем, как «testrequest.txt» в той же директории, где находится HTTPClient.class, после чего запустим программу:

java HTTPClient testrequest.txt testreply.txt

Если всё пройдёт нормально, сервер devresource.org доступен и работает сеть, через некоторое время вы получите в той же директории файл «testreply.txt», содержащий ответ сервера, включая HTTP заголовок и содержимое документа. Файл «testreply.txt» вы сможете посмотреть в любом текстовом редакторе.

Как создать мэнеджер докачки.

Немного модифицировав программу HTTP клиента, вы сможете получить, к примеру, менеджер докачки. Принципы того, как закачать файл не целиком, а только его кусочек, я дал в предыдущей части статьи. Но, на всякий случай, напомню, как это делается:

вначале методом HEAD получаем всю доступную информацию о файле:

HEAD http://www.devresource.org/JavaPower.gif HTTP/1.1
Host: www.devresource.org

из полученного ответа выделяем значение параметра

Content-Length: 6776

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

GET http://www.devresource.org/JavaPower.gif HTTP/1.1
Host: www.devresource.org
Range-Unit: 3388 | 1024

если полученный ответ содержит код «206 Partial Content», то всё, что содержится под заголовком, и будет запрашиваемым куском файла.

Простой WEB сервер

WEB узлы отвечают за выдачу информации HTTP клиентам. Задача следующего примера показать, как работает WEB сервер. Для того, чтобы не слишком перегружать код, заранее ограничим её функционально:

сервер будет принимать заголовки длиною не более 64 кб
сервер понимает только методы GET и POST, в противном случае выдаётся «400 Bad Request»
сервер не сможет выдавать документ по-частям (для мэнеджеров докачек)
допустимые коды возвратов для сервера ограничиваются «200 OK», если он удачно обработал запрос, «400 Bad Request», если запрос не понят сервером и «404 Not Found», если запрос понят, но файл не найден
результатом работы сервера, будет выдача файла, указанного в запросе. При этом всё, что находится после символа «?» в URI документа и сам этот символ отсекаются.
MIME типы, выдаваемые сервером ограничены 5 значениями: text/html для файлов с расширениями htm и html, image/jpeg, image/gif, image/x-xbitmap для файлов с расширениями jpg, gif, bmp соответственно и text/plain для всех остальных файлов

Программа SimpleWEBServer была создана как модификация SampleServer из первой части статьи:

SimpleWEBServer.java

import java.io.*;
import java.net.*;
import java.text.DateFormat;
import java.util.Date;
import java.util.TimeZone;

class SimpleWEBServer extends Thread
{
Socket s;

public static void main(String args[])
{
try
{
// привинтить сокет на локалхост, порт 80
ServerSocket server = new ServerSocket(80, 0,
InetAddress.getByName(«localhost»));

System.out.println(«server is started»);

// слушаем порт
while(true)
{
// ждём нового подключения, после чего запускаем обработку клиента
// в новый вычислительный поток
new SimpleWEBServer(server.accept());
}
}
catch(Exception e)
{System.out.println(«init error: «+e);} // вывод исключений
}

public SimpleWEBServer(Socket s)
{
this.s = s;

// и запускаем новый вычислительный поток (см. ф-ю run())
setDaemon(true);
setPriority(NORM_PRIORITY);
start();
}

public void run()
{
try
{
// из сокета клиента берём поток входящих данных
InputStream is = s.getInputStream();
// и оттуда же — поток данных от сервера к клиенту
OutputStream os = s.getOutputStream();

// буффер данных в 64 килобайта
byte buf[] = new byte[64*1024];
// читаем 64кб от клиента, результат — кол-во реально принятых данных
int r = is.read(buf);

// создаём строку, содержащую полученую от клиента информацию
String request = new String(buf, 0, r);

// получаем путь до документа (см. ниже ф-ю «getPath»)
String path = getPath(request);

// если из запроса не удалось выделить путь, то
// возвращаем «400 Bad Request»
if(path == null)
{
// первая строка ответа
String response = «HTTP/1.1 400 Bad Request\n»;

// дата в GMT
DateFormat df = DateFormat.getTimeInstance();
df.setTimeZone(TimeZone.getTimeZone(«GMT»));
response = response + «Date: » + df.format(new Date()) + «\n»;

// остальные заголовки
response = response
+ «Connection: close\n»
+ «Server: SimpleWEBServer\n»
+ «Pragma: no-cache\n\n»;

// выводим данные:
os.write(response.getBytes());

// завершаем соединение
s.close();

// выход
return;
}

// если файл существует и является директорией,
// то ищем индексный файл index.html
File f = new File(path);
boolean flag = !f.exists();
if(!flag) if(f.isDirectory())
{
if(path.lastIndexOf(«»+File.separator) == path.length()-1)
path = path + «index.html»;
else
path = path + File.separator + «index.html»;
f = new File(path);
flag = !f.exists();
}

// если по указанному пути файл не найден
// то выводим ошибку «404 Not Found»
if(flag)
{
// первая строка ответа
String response = «HTTP/1.1 404 Not Found\n»;

// дата в GMT
DateFormat df = DateFormat.getTimeInstance();
df.setTimeZone(TimeZone.getTimeZone(«GMT»));
response = response + «Date: » + df.format(new Date()) + «\n»;

// остальные заголовки
response = response
+ «Content-Type: text/plain\n»
+ «Connection: close\n»
+ «Server: SimpleWEBServer\n»
+ «Pragma: no-cache\n\n»;

// и гневное сообщение
response = response + «File » + path + » not found!»;

// выводим данные:
os.write(response.getBytes());

// завершаем соединение
s.close();

// выход
return;
}

// определяем MIME файла по расширению
// MIME по умолчанию — «text/plain»
String mime = «text/plain»;

// выделяем у файла расширение (по точке)
r = path.lastIndexOf(«.»);
if(r > 0)
{
String ext = path.substring(r);
if(ext.equalsIgnoreCase(«html»))
mime = «text/html»;
else if(ext.equalsIgnoreCase(«htm»))
mime = «text/html»;
else if(ext.equalsIgnoreCase(«gif»))
mime = «image/gif»;
else if(ext.equalsIgnoreCase(«jpg»))
mime = «image/jpeg»;
else if(ext.equalsIgnoreCase(«jpeg»))
mime = «image/jpeg»;
else if(ext.equalsIgnoreCase(«bmp»))
mime = «image/x-xbitmap»;
}

// создаём ответ

// первая строка ответа
String response = «HTTP/1.1 200 OK\n»;

// дата создания в GMT
DateFormat df = DateFormat.getTimeInstance();
df.setTimeZone(TimeZone.getTimeZone(«GMT»));

// время последней модификации файла в GMT
response = response + «Last-Modified: » + df.format(new Date(f.lastModified())) + «\n»;

// длина файла
response = response + «Content-Length: » + f.length() + «\n»;

// строка с MIME кодировкой
response = response + «Content-Type: » + mime + «\n»;

// остальные заголовки
response = response
+ «Connection: close\n»
+ «Server: SimpleWEBServer\n\n»;

// выводим заголовок:
os.write(response.getBytes());

// и сам файл:
FileInputStream fis = new FileInputStream(path);
r = 1;
while(r > 0)
{
r = fis.read(buf);
if(r > 0) os.write(buf, 0, r);
}
fis.close();

// завершаем соединение
s.close();
}
catch(Exception e)
{e.printStackTrace();} // вывод исключений
}

// «вырезает» из HTTP заголовка URI ресурса и конвертирует его в filepath
// URI берётся только для GET и POST запросов, иначе возвращается null
protected String getPath(String header)
{
// ищем URI, указанный в HTTP запросе
// URI ищется только для методов POST и GET, иначе возвращается null
String URI = extract(header, «GET «, » «), path;
if(URI == null) URI = extract(header, «POST «, » «);
if(URI == null) return null;

// если URI записан вместе с именем протокола
// то удаляем протокол и имя хоста
path = URI.toLowerCase();
if(path.indexOf(«http://», 0) == 0)
{
URI = URI.substring(7);
URI = URI.substring(URI.indexOf(«/», 0));
}
else if(path.indexOf(«/», 0) == 0)
URI = URI.substring(1); // если URI начинается с символа /, удаляем его

// отсекаем из URI часть запроса, идущего после символов ? и #
int i = URI.indexOf(«?»);
if(i > 0) URI = URI.substring(0, i);
i = URI.indexOf(«#»);
if(i > 0) URI = URI.substring(0, i);

// конвертируем URI в путь до документов
// предполагается, что документы лежат там же, где и сервер
// иначе ниже нужно переопределить path
path = «.» + File.separator;
char a;
for(i = 0; i < URI.length(); i++)
{
a = URI.charAt(i);
if(a == ‘/’)
path = path + File.separator;
else
path = path + a;
}

return path;
}

// «вырезает» из строки str часть, находящуюся между строками start и end
// если строки end нет, то берётся строка после start
// если кусок не найден, возвращается null
// для поиска берётся строка до «\n\n» или «\r\n\r\n», если таковые присутствуют
protected String extract(String str, String start, String end)
{
int s = str.indexOf(«\n\n», 0), e;
if(s < 0) s = str.indexOf(«\r\n\r\n», 0);
if(s > 0) str = str.substring(0, s);
s = str.indexOf(start, 0)+start.length();
if(s < start.length()) return null;
e = str.indexOf(end, s);
if(e < 0) e = str.length();
return (str.substring(s, e)).trim();
}
}

Компилируем программу и получаем SimpleWEBServer.class. Так как данная программа была написана с использованием Java 2 API (в части получения даты в формате GMT), то для её компиляции и выполнения нужен JDK версии не ниже 1.2. Данный сервер будет ставиться на localhost:80, но, в принципе, можно использовать любой другой свободный порт и адрес хоста.

Запускаем сервер:

java SimpleWEBServer

Если программа написала «server is started», то сервер запущен и готов к работе. В противном случае, будет выдана ошибка (скорее всего, что даный порт занят другой программой или запрещён политикой безопасности).

Проверьте сервер: положите в директорию, где находится программа файл «index.html». Файл может быть, допустим, таким:

<html><head>test file</head>
<body>
<center> <h1> This a test!!! </h1> </center>
</body></html>

Теперь откройте браузер и наберите адрес «http://localhost» или «http://localhost/index.html». Страничка должна отобразиться.

Proxy серверы

Proxy (proxy — заместитель, посредник) серверы (в просторечье — просто «прокси» или «прокси сервер») — это узловые станции интернета. Они отвечают за соединение различных сегментов интернета меджду собою, а так же могут выполнять несколько других полезных действий. Хотя функционально различные варианты прокси перекрывают друг друга, всё же можно выделить несколько их основных типов.

Первый тип прокси — это так называемый «шлюз». Как было сказанно в первой части статьи, IP адрес для каждой TCP/IP сети должен быть уникальным. Каждый IP в сети интернет тоже уникален, по этому возникает закономерный вопрос подключения частной локальной сети к сети интернет. Этот вопрос как раз и решают шлюзы (они же proxy-gate). Программа такого прокси устанавливается на одном из серверов внутренней сети, имеющий выход в Internet. Разберём принцип работы такого прокси.

Допустим, хост прокси имеет следующий IP адрес во внутренней локальной сети — «127.0.0.2″, а порт, на который он установлен — 3128 (наиболее часто используются под прокси следующие порты — 81, 3128, 8080, 8081). Допустим, что клиент, находящийся во внутренней подсети, запрашивает страницу с URL «http://www.devresource.org/». Тогда происходит следующее:

клиент открывает сокет локальной сети до прокси сервера («127.0.0.2:3128″)
в открытый сокет клиент пишет HTTP запрос примерно следующего содержания:

GET http://www.devresource.org/ HTTP/1.1
Host: www.devresource.org

прокси сервер получает этот запрос, из параметра «Host» узнаёт хост ресурса, его порт и открывает сокет сети Internet до сервера «www.devresource.org:80″
в открытый сокет, прокси-сервер пишет полученый от клиента HTTP запрос; фактически он перенаправляет запрос от клиента к серверу, не изменяя его.
сервер запрашиваемого ресурса получает HTTP запрос от шлюза, обрабатывает его и высылает ответ обратно, к прокси серверу
шлюз получает ответ от сервера «www.devresource.org:80″ и, не изменяя его, отправляет к клиенту
клиент получает ответ от сокета прокси сервера и обрабатывает его

Для лучшего понимания того, что пошагово описано выше, прилагаю схему, иллюстрирующую процесс шлюзования запроса:

 

Схема работы шлюза.
Схема работы шлюза. Следующим типом прокси является «анонимный прокси» или анонимайзер. Принцип его работы схож с работой шлюза, но задача немного не та: задачей анонимайзера является скрыть IP адрес клиента. Этот прокси не пересылает запросы между разными TCP/IP сетями, он просто выступает посредником между клиентом и запрашиваемым хостом.

Анонимный прокси всё так же получает запрос от клиента, обрабатывает поле «Host», передаёт запрос серверу и возвращает его ответ. Единственное отличие в том, что и ServerSocket, и Socket до указанного хоста лежат в одной сети. Собственно, анонимный прокси является самым простым типом прокси-серверов.

Ещё одним типом прокси являются так называемые «Firewall» (firewall — огненная стена, в просторечье — файрвол). Это модули системы защиты компьютеров и локальных сетей. Для HTTP суть этих модулей сводится к тому, что они фильтруют нежелательный контент. Например рассмотрим принцип работы простейшего HTTP Firewall, отсекаюего загрузку любых не-текстовых документов и запрещающий запрос страничек, URL которых содержит в себе ключевые слова «sex», «chat» и т.д. У многих на работе стоят подобные файрволы (обычно они функционально совмещены с шлюзами). Зная, как они работают, можно попытаться обойти их.

Итак, разберём шаги, предпринимаемые HTTP файрволом:

клиент открывает сокет локальной сети до прокси сервера и отправляет ему заголовок
файрвол обрабатывает HTTP заголовок запроса: выделяет URL ресурса и сканирует его на наличие «запретных» слов.
если слова найдены — возвращает клиенту ошибку типа «403 Forbidden» и завершает с ним соединение
если URL ресурса «в порядке», то соединяется с указанным хостом и передаёт ему запрос, в котором подменяет метод запроса на «HEAD». Например так:

HEAD [URI] HTTP/1.1
Host: [host]

прокси получает часть ответа запрашиваемого сервера и обрабатывает HTTP заголовок ответа: выделяет поле «Content-Type», читает MIME тип документа (по умолчанию, если заголовок «Content-Type» опущен, то считается, что MIME тип — «text/html»).
если заголовок содержит код возврата отличный от «200 OK», прокси создаёт страничку с информацией об ошибке и отправляет её к клиенту.
Если заголовок содержит MIME типа «image/gif» — то есть класса «image/», то в ответ выдаётся заранее заготовленная GIF картинка, содержащая прозрачный пиксел:

HTTP/1.1 200 OK
Content-Type: image/gif
Content-Length: [размер файла в байтах]

[код картинки]

Впрочем, прокси может просто выдать ошибку типа «403 Forbidden» — всё зависит от качества программы.
если заголовок содержит MIME типа «text/html» — то есть класса «text/», то прокси перенаправляет запрос клиента серверу, после чего перенаправляет ответ сервера к клиенту (работает как простой посредник)
во всех остальных случаях к клиенту возвращается страничка с ошибкой «403 Forbidden»

Рассмотрим ещё один тип прокси, называемый кэширующим (cache-proxy).

Что это значит кэширующий прокси? Это значит, что такой прокси сохраняет некоторые странички к себе в память, а потом, если от клиента придёт запрос на эту страничку, файл будет загружён не из сети, а из кэша — области памяти, куда прокси сохранил страничку.

Зачем это нужно? Данный принцип значительно сокращает траффик, ведь стоит одному клиенту обратиться, скажем, к «http://www.devresource.org/», как страничка окажется в кэше и для всех следующих клиентов, работающих через данный прокси и запрашивающих «http://www.devresource.org/», серверу не нужно будет снова загружать эту страничку из сети: достаточно будет просто достать её из кэша.

Какие странички нужно кэшировать, а какие — нет регламентируется следующими правилами: прежде всего, сохраняются только те странички, что были получены методом GET запроса. Кроме того, сохранять или не сохранять в кэш регламентируют такие поля HTTP, как «Pragma» и «Cache-Control». Встречаются эти поля, как вы помните, и в HTTP запросе и в HTTP ответе.

Вспомним ещё раз значения полей «Pragma» и «Cache-Control»:

«public» — документ является публичным, его может брать любой клиент из кэша
«private» — документ является приватным, из кэша его может брать только клиент, пославший этот запрос
«no-store» — не сохранять документ в кэш
«no-transform» — если в кэшэ уже находится документ по данному запросу, то его не нужно обновлять ответом на этот запрос сервера
«must-revalidate» — в любом случае, лежит этот документ в кэшэ или нет, прокси обязан обновить его на тот, что выдаст сервер
«proxy-revalidate» — относительно прокси-сервера означает то же самое
«max-age=[seconds]» — количество секунд, которое должен храниться этот документ в кэшэ, начиная от данного момента

И, как вы помните, значения полей можно совмещать, к примеру:

Pragma: must-revalidate, private, max-age=86400

будет обозначать, что прокси обязан сохранить этот документ в кэш, причём только для этого клиента. И что по истечении суток от данного момента (через 86400 секунд) документ из кэша должен быть удалён.

Внимание! Для прокси-сервера приоритетным является поле «Cache-Control». В случае противоречивых данных в полях «Cache-Control» и «Pragma», прокси сервер будет выполнять команды первого поля, а браузер будет конфигурировать свой кэш в соответствии с полем «Pragma».

Простой кэширующий прокси

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

Чтобы не усложнять код, программа имеет следующие ограничения:

длина HTTP запроса от клиента не должна превышать 64 кб (чтобы не обрабатывать поле «Content-Length» при POST запросах)
обрабатываются только поле «Pragma» из HTTP заголовка запроса; обработка эта заключается в поиске параметра «no-cache»
кэш прокси хранится в форме файлов-директорий. К примеру страничка с URL «http://www.devresource.ru/javalinks/catalog.php3?val=this a test!» в системе Windows будет сохранена как «.\cache\www.devresource.org\!javalinks\catalog.php3\val=this%20a%20test%21″ — как видно, символы «/» и «?» заменяются, соответственно, на «\!» и «\» (вместо «\» может быть любой символ разделителя пути — берётся java.io.File.separatorChar), а служебный символ «!» обозначающий директорию в кэше и непечатные символы — на %NN, в соответствие с кодом символа.

Программа CacheProxy была тоже создана как модификация SampleServer из первой части статьи:

CacheProxy.java

import java.io.*;
import java.net.*;

class CacheProxy extends Thread
{
Socket s; // сокет подключения
InputStream is; // входящий поток от сокета
OutputStream os; // исходящий поток от сокета

// пытается подключиться как сервер на адрес localhost порт 3128
// после чего сидит и ждёт подключений от браузера
// каждое новое подключение передаёт в обработку отдельному вычислительному потоку
public static void main(String args[])
{
try
{
// bind to «localhost:3128″
ServerSocket s = new ServerSocket(3128, 0, InetAddress.getByName(«localhost»));

System.out.println(«proxy is started»);

// listen port
while(true)
{
try {new CacheProxy(s.accept());} // process new client in new thread
catch(Exception ex) {}
}
}
catch(Exception e)
{System.out.println(«main init error: «+e);} // by socket binding error
}

// конструктор потока обработки подключения
public CacheProxy(Socket s) throws Exception
{
this.s = s;

// start thread
setDaemon(true);
setPriority(NORM_PRIORITY);
start();
}

// «вырезает» из строки str часть, находящуюся между строками start и end
// если строки end нет, то берётся строка после start
// если кусок не найден, возвращается null
// для поиска берётся строка до «\n\n» или «\r\n\r\n», если таковые присутствуют
protected String extract(String str, String start, String end)
{
int s = str.indexOf(«\n\n», 0), e;
if(s < 0) s = str.indexOf(«\r\n\r\n», 0);
if(s > 0) str = str.substring(0, s);
s = str.indexOf(start, 0)+start.length();
if(s < start.length()) return null;
e = str.indexOf(end, s);
if(e < 0) e = str.length();
return (str.substring(s, e)).trim();
}

// «вырезает» из HTTP заголовка URI ресурса и конвертирует его в filepath для файла кэша
// URI берётся только для GET и POST запросов, иначе возвращается null
protected String getPath(String header)
{
String URI = extract(header, «GET «, » «), path;
if(URI == null) URI = extract(header, «POST «, » «);
if(URI == null) return null;

path = URI.toLowerCase();
if(path.indexOf(«http://», 0) == 0)
URI = URI.substring(7);
else
{
path = extract(header, «Host:», «\n»);
if(path == null) return null;
URI = path+URI;
}

// define cashe path
path = «cache»+File.separator;

// convert URI to filepath
char a;
boolean flag = false;
for(int i = 0; i < URI.length(); i++)
{
a = URI.charAt(i);

switch(a)
{
case ‘/’ :
if(flag)
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
else
path = path+».!»+File.separatorChar;
break;
case ‘!’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘\\’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘:’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘*’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘?’ :
if(flag)
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
else
{
path = path+».!»+File.separatorChar;
flag = true;
}
break;
case ‘»‘ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘<’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘>’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
case ‘|’ :
path = path+»%»+Integer.toString((int)a, 16).toUpperCase();
break;
default: path = path+a;
}
}
if(path.charAt(path.length()-1) == File.separatorChar) path = path+».root»;

return path;
}

// печатает ошибку прокси
protected void printError(String err) throws Exception
{
os.write((new String(«HTTP/1.1 200 OK\nServer: HomeProxy\n»
+»Content-Type: text/plain; charset=windows-1251\n\n»
+err)).getBytes());
}

// загружает из сети страничку с одновременным кэшированием её на диск
// странички в кэше храняться прямо с HTTP заголовком
protected void from_net(String header, String host, int port, String path) throws Exception
{
Socket sc = new Socket(host, port);
sc.getOutputStream().write(header.getBytes());

InputStream is = sc.getInputStream();

File f = new File((new File(path)).getParent());
if(!f.exists()) f.mkdirs();

FileOutputStream fos = new FileOutputStream(path);

byte buf[] = new byte[64*1024];
int r = 1;
while(r > 0)
{
r = is.read(buf);
if(r > 0)
{
fos.write(buf, 0, r);
if(r > 0) os.write(buf, 0, r);
}
}
fos.close();
sc.close();
}

// вытаскивает из HTTP заголовка хост, порт соединения и путь до файла кэша,
// после чего вызывает ф-ию загрузки из сети
protected void from_net(String header) throws Exception
{
String host = extract(header, «Host:», «\n»), path = getPath(header);
if((host == null)||(path == null))
{
printError(«invalid request:\n»+header);
return;
}

int port = host.indexOf(«:»,0);
if(port < 0) port = 80;
else
{
port = Integer.parseInt(host.substring(port+1));
host = host.substring(0, port);
}

from_net(header, host, port, path);
}

// загружает из кэша файл и выдаёт его
// если во входящем HTTP заголовке стоит «Pragma: no-cache»
// или такого файла в кэше нет, то вызывается ф-ия загрузки из сети
protected void from_cache(String header) throws Exception
{
String path = getPath(header);
if(path == null)
{
printError(«invalid request:\n»+header);
return;
}

// except «Pragma: no-cache»
String pragma = extract(header, «Pragma:», «\n»);
if(pragma != null)
if(pragma.toLowerCase().equals(«no-cache»))
{
from_net(header);
return;
}

if((new File(path)).exists())
{
FileInputStream fis = new FileInputStream(path);
byte buf[] = new byte[64*1024];
int r = 1;

while(r > 0)
{
r = fis.read(buf);
if(r > 0) os.write(buf, 0, r);
}

fis.close();
}
else
from_net(header);
}

// обработка подключения «в потоке»
// получает HTTP запрос от браузера
// если запрос начинается с GET пытается взять файл из кэша
// иначе — грузит из сети
public void run()
{
try
{
is = s.getInputStream();
os = s.getOutputStream();

byte buf[] = new byte[64*1024];
int r = is.read(buf);

String header = new String(buf, 0, r);
if(header.indexOf(«GET «, 0) == 0)
from_cache(header);
else
from_net(header);

s.close();
}
catch(Exception e)
{
try
{
e.printStackTrace();
printError(«exception:\n»+e);
s.close();
}
catch(Exception ex){}
}
}
}

После компиляции программы, получаем CacheProxy.class и запускаем его (перед запуском убедитесь, что порт 3128 на вашем localhost свободен):

java CacheProxy

Если выдана строка «proxy is started», то прокси был успешно запущен.

Теперь можно проверить его в действии: в настройках своего браузера найдите секцию, где прописываются Proxy сервера и укажите следующий HTTP прокси — имя localhost, порт — 3128. Теперь откройте браузер и немного поползайте по WEB ресурсам. Из-за того, что программа имеет ограниченную функциональность, некоторые ресурсы могут не открыться или выдать ошибку. После завершения работы, откройте в той же директории, где лежит программа, папку «cache» и посмотрите, как прокси сохранил просмотренные вами странички.

Сервлеты

Сервлеты являются специализированным механизмом Java для создания WEB ресурсов.

Сервлеты входят в пакеты javax.servlet, javax.servlet.http, javax.servlet.jsp; пакеты эти, в свою очередь, принадлежат набору Java Servlet API, который входит в архитектуру Java 2 Enterprise Edition. Для работы с сервлетами вам потребуется либо J2EE, либо Java Servlet API, поставляемый вместе с такими WEB серверами, как Jacarta Tomcat.

Что такое сервлеты? Сервлеты, фактически, это модули обработки HTTP и FTP запросов, используемые для построения порталов (web gates).

Основой этих порталов является собственно WEB сервер — программа, которая держит сокет сервера, принимает и передаёт данные. Чаще всего, для ускорения работы, сервер бывает написан не на Java, а на каком-либо другом языке программирования (например на C++).

В связке с сервером работает базовый сервлет. Именно ему отправляет сервер данные и от него же получает ответ, отправляемый клиенту. Фактически, базовый сервлет является «мозгом» сервера. Основная функция этого сервлета — прочитать запрос клиента, расшифровать его и, в соответствиии с расшифровкой, передать работу сервлету, отвечающему за этот тип запрашиваемой информации. Зачастую, для достижения скорости, роль базового сервлета играет сам сервер. Именно по такой схеме работает, скажем, Jacarta Tomcat.

 

Схема работы сервлетов и сервера.
Схема работы сервлетов и сервера.На рисунке изображена схема передачи вызовов (request) и ответов (response) между сервером и сервлетами. Данная схема изображает работу HTTP сервера, который имеет несколько JSP страниц и два ресурса «/sample1″ и «/sample2″, за обработку которых отвечает два сервлета — «Sample1 Servlet» и «Sample2 Servlet» соответственно.

Разберём пошагово то, что изображено на рисунке:

  1. клиент подсоединяется к серверу
  2. сервер передаёт запрос (request) базовому сервлету («Basic Servlet»)
  3. базовый сервлет вычеленяет из запроса URI ресурса
    • если URI указывает на «/sample1″, то запрос целиком (без изменений) передаётся сервлету «Sample1 Servlet», который, в дальнейшем, и обрабатывает этот запрос
    • если URI указывает на «/sample2″, сервер передаёт запрос сервлету «Sample2 Servlet»
    • во всех остальных случаях запрос передаётся модулю «JSP Servlet»
  4. сервлет, которому было передано управление, обрабатывает данные, создаёт ответ (response), после чего ответ отсылается обратно базовому сервлету.
  5. базовый сервлет, не обрабатывая полученные данные, тут же пересылает их обратно серверу
  6. сервер выдаёт данные клиенту

Таким образом достигается разбиение задачи обработки запроса на логические части, за каждую из которых отвечает свой модуль, свой «програмный кирпичик». На самом деле, ступеней в обработке запроса может быть гораздо больше. К примеру за методы «GET» и «POST» могут отвечать разные модули.

Интерфейс Servlet

Объединяет все эти модули то, что они сквозным образом связанны между собою с помощью интерфейса javax.servlet.Servlet

Посмотрим на этот интерфейс. В нём указано всего 5 методов:

public void init(ServletConfig config) throws ServletException

Этот метод вызывается, чтобы проинформировать сервлет о том, что он включён как модуль для обслуживания запросов клиента. Параметр config разделяет интерфейс javax.servlet.ServletConfig, несущий информацию об окружении сервера, имени сервлета, начальных параметрах и прочих плюшках. Об интерфейсе javax.servlet.ServletConfig будет рассказано чуть далее. Предполагается, что после вызова этой функции, сервлет аккуратно сохранит этот config у себя в переменной и будет выдавать его с помощью другого метода:

public ServletConfig getServletConfig()

Получив системную информацию с помощью «getServletConfig()», сервер может захотеть узнать имя автора, дату создания, прочую информацию о сервлете, что и достигается вызовом

public String getServletInfo()

Чтобы обработать запрос и получить результат его обработки, используется функция

public void service(ServletRequest request, ServletResponse response)
	throws ServletException, java.io.IOException

В этой функции коду, который будет обрабатывать данные, передаются два инструмента: один — для получения данных от сервера, другой — для отправки результата работы сервлета. Соответственно это параметры request и response, разделяющие интерфейсы javax.servlet.ServletRequest и javax.servlet.ServletResponse Вся работа с данными ведётся именно через эти интерфейсы, так что далее поговорим о них подробнее.

После того, как сервер перестал нуждаться в этом модуле вызывается метод

public void destroy()

который и завершает все операции с объектом сервлета.

Интерфейс ServletConfig

4 метода, имена которых говорят сами за себя, составляют суть интерфейса javax.servlet.ServletConfig:

public String getServletName()
public ServletContext getServletContext()
public String getInitParameter(String name)
public java.util.Enumeration getInitParameterNames()

Думаю, назначение всех функция понятно, кроме

public ServletContext getServletContext()

Этот метод возвращает ссылку на очень полезный инструмент для работы с сервером:

Интерфейс ServletContext

ServletContext — интерфейс, определяющий доступ к следующим полезнейшим функциям:

public Object getAttribute(String name)
public java.util.Enumeration getAttributeNames()
public void setAttribute(String name, Object object)
public void removeAttribute(String name)

Четыре метода для работы с аттрибутами. Роль аттрибутов выполняет любой объект любого класса. Цель данных функций — пересылать между несвязанными друг с другом сервлетами разные объекты.

public String getInitParameter(String name)
public java.util.Enumeration getInitParameterNames()

Доступ к параметрам, с которыми был запущен сервер. Тут же могут лежать имя хоста, порт и прочие полезности.

public int getMajorVersion()
public int getMinorVersion()

Возвращает версии Servlet API.

public String getMimeType(String file)

Возвращает MIME тип ассоциированный с файлом, путь до которого указан в переменной file. Вспомните, как пришлось определять MIME в программе SimpleWEBServer и оцените удобство!

public java.util.Set getResourcePaths()
public java.net.URL getResource(String path) throws java.net.MalformedURLException
public InputStream getResourceAsStream(String path)

Возвращает пути к доступным для сервера ресурсам и сами ресурсы в виде URL и в виде потоков данных.

public RequestDispatcher getRequestDispatcher(path)
public RequestDispatcher getNamedDispatcher(name)

RequestDispatcher — это инструмент для того, чтобы переслать запрос другому ресурсу. Эти функции нужны, чтобы получить объект этого инструмента для указанных ресурсов. То бишь, скажем, для того, чтобы перенаправить запрос сервлету «sample1″ из тела сервлета, можно сделать так:

getServletConfig().getServletContext().getNamedDispatcher(«sample1″).forward(request, response);

Собственно класс RequestDispatcher включает в себя лишь два метода:

public void forward(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException

public void include(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException

Причём первый — для перенаправления запроса, а второй — для включения результата работы вызываемого сервлета в результат работы текущего. К примеру, сервлет 1 печатает слово «test 1″, потом вызывает include для сервлета два, после чего печатает слово «test 2″. Сервлет 2 же просто печатает слово » and «. Результатом работы сервлета 1 будет строка «test 1 and test 2″.

public void log(String msg)

Записать что-то в лог сервера.

public void log(String message, Throwable throwable)

Определить исключение и фразу, которая будет записываться в лог по получении этого исключения.

public String getRealPath(String path)

Переводит путь типа «/index.html» в «http://host/contextPath/index.html»

public String getServerInfo()

Возвращает имя сервера.

public ServletContext getContext(String uripath)

Этот метод позволяет обмениваться ServletContext между разными ресурсами одного и того же сервера.

public String getServletContextName()

Возвращает имя сервлета, которому принадлежит данный объект интерфейса ServletContect.

Интерфейс ServletRequest

Интерфейс ServletRequest — это инструмент для получения параметров HTTP запроса. Этот интерфейс имеет некоторые методы, идентичные по названию и назначению с ServletContext:

public Object getAttribute(String name)
public java.util.Enumeration getAttributeNames()
public void setAttribute(String name, Object o)
public void removeAttribute(java.lang.String name)
public String getServerName()
public RequestDispatcher getRequestDispatcher(String path)

Оставшися методы позволяют с удобством работать с HTTP заголовком запроса:

public String getCharacterEncoding()
public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException

Работа с кодировкой символов в полях HTTP заголовка. Функции задают метод расшифровки CGI запросов из формы %NN в обычные символы. К примеру, какой стандарт — KOI8-R, windows-1251 или UTF-8 нужно применить для расшифровки кириллических символов.

public int getContentLength()
public String getContentType()

Читает поля «Content-Length», «Content-Type» из HTTP запроса.

public jString getParameter(String name)
public java.util.Enumeration getParameterNames()
public String[] getParameterValues(String name)
public java.util.Map getParameterMap()

Функции для получения поля из HTTP заголовка и его значения.

public ServletInputStream getInputStream() throws java.io.IOException
public java.io.BufferedReader getReader() throws java.io.IOException

Получить входящий поток данных или его «читатель». Reader применяется для чтения текстовой информации — он автоматически расшифрует строки в соответствии с заданным charset. Внимание! В версии J2EE 1.3 имеется существенный баг: при расшифровке символа %25 (символ % в Post и Get запросах) Reader выдаёт ошибку (баг замечен на серверах Tomcat 4 и Resign). Возможно, что схожий баг есть и с другими символами.

public String getProtocol()

Получить версию HTTP протокола запроса (к примеру — «HTTP/1.1″).

public String getScheme()

Возвращяет имя схемы запроса. Например «http», «https», или «ftp».

public int getServerPort()
public String getRemoteAddr()
public String getRemoteHost()
public boolean isSecure()

Порт сервера, IP адрес клиента, имя хоста клиента и является ли соединение скретным (по протоколу HTTPS)

public java.util.Locale getLocale()
public java.util.Enumeration getLocales()

Предпочитаемый клиентом язык документа (результат обработки поля «Accept-Language»)

Интерфейс ServletResponse

Интерфейс ServletResponse — это инструмент для отправки данных клиенту. Все методы данного инструмента служат именно этой цели:

public java.lang.String getCharacterEncoding()
public void setLocale(java.util.Locale loc)
public java.util.Locale getLocale()

Первый метод возвращает MIME тип кодировки (к примеру — UTF8), в которой будет выдаваться информация. Вторые два метода тоже работают с charset. Они указывают на язык используемый в документе (например — русский).

public ServletOutputStream getOutputStream() throws java.io.IOException

Возвращает поток вывода данных для сервлета. Этот поток используется, к примеру, для вывода бинарных файлов. Текстовые данные можно выводить с помощью java.io.Writer:

public java.io.PrintWriter getWriter() throws java.io.IOException

Этот метод автоматически конвертирует строки в тот charset, что указан в методе getCharacterEncoding() и getLocale().

public void setContentLength(int len)

Этим методом устанавливается значение поля HTTP заголовка «Content-Length»

public void setContentType(String type)

Метод для отправки MIME типа содержимого документа. Поле HTTP заголовка «Content-Type».

public void setBufferSize(int size)
public int getBufferSize()
public void flushBuffer() throws java.io.IOException
public void resetBuffer()

Дело в том, что поток вывода данных является буфферизованным. Это означает, что следующая порция данных будет выдана клиенту только после заполнения буфера. Указанные методы позволяют, соответственно, установить размер буффера отправки, получить его размер, инициализировать отправку содержимое буффера клиенту, не дожидаясь его заполнения, а так же очистить этот буффер от данных.

public boolean isCommitted()

Этим методом можно получить флаг, начата ли уже отправка данных клиенту. Флаг будет положительным, если HTTP заголовок ответа был уже отправлен.

public void reset()

Если HTTP заголовок ещё не отправлен, то этот метод «сбрасывает» HTTP заголовок к значениям «по умолчанию».

Предопределённые типы сервлетов

Java Servlet API, кроме собственно интерфейсов, так же содержит несколько классов сервлетов, которые могут служить основой для ваших программ.

Базовым для всех этих классов является абстрактный класс javax.servlet.GenericServlet :

public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable

Как видно из определения этого класса, он имеет все методы интерфейсов Servlet и ServletConfig. Не реализованным методом остался только

public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, java.io.IOException

который и был объявлен абстрактным.

На базе этого класса был создан другой абстрактный класс — javax.servlet.http.HttpServlet :

public abstract class HttpServlet extends GenericServlet implements java.io.Serializable

Создан этот класс был в соответствии с концепцией «ещё больше удобств для программиста» и имеет много полезных методов:

protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, java.io.IOException
protected void service(ServletRequest req, ServletResponse res)
throws ServletException, java.io.IOException

Различные варианты service(ServletRequest req, ServletResponse res) для разных HTTP методов от DELETE и GET до PUT и TRACE. А чтобы с удобством получать данные по CGI интерфейсу не расшифровывывая заголовок были созданы классы HttpServletRequest и HttpServletResponse, входящие вместе с HttpServlet в пакет javax.servlet.http

protected long getLastModified(HttpServletRequest req)

Этот метод возвращает время последней модификации объекта HttpServletRequest. Значение времени он берёт из поля «Date» HTTP заголовка запроса. Если же поле не обнаружено, то возвращает -1.

Соответственно разберём и интерфейсы HttpServletRequest и HttpServletResponse. Они являются наследниками соответственно ServletRequest и ServletResponse.

HttpServletRequest помимо методов, унаследованных от ServletRequest, имеет так же следующие полезнейшие методы:

Cookie[] getCookies()

Возвращает набор куков, пересланных клиентом серверу.

Класс Cookie, входящий в тот же пакет javax.servlet.http, содержит всю возможную информацию о куке. Важнейшими методами этого класса являются

int getMaxAge()
String getName()
String getValue()

выдающие, соответственно, сколько ещё времени этому куку осталось жить, имя кука и его значение. Так же

Cookie(String name, String value)
void setValue(String newValue)
void setMaxAge(int expiry)

для создания кука, установки его значения и максимального возраста.

long getDateHeader(String name)

Возвращает дату из HTTP заголовка, если таковая есть.

int getIntHeader(java.lang.String name)

Возвращает численное значения поля с именем name из HTTP заголовка запроса

String getMethod()

Возвращает метод HTTP запроса.

String getQueryString()
String getRequestURI()
StringBuffer getRequestURL()

Возвращает строку, содержащуюся в URL документа после символа «?», URI документа и полный URL.

HttpSession getSession()
HttpSession getSession(boolean create)
boolean isRequestedSessionIdFromCookie()
boolean isRequestedSessionIdFromURL()
boolean isRequestedSessionIdValid()

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

Сессии необходимы для того, чтобы таскать за пользователем данные из страницы в страницу. К примеру, пользователь заходит на страницу (1), где ему отправляются некоторые данные для страницы (2), а та сохраняет ещё какие-то вещи для страницы (3).

Впринципе, на странице (1) можно выслать данные пользователю, потом получить их на странице (2), добавить что-то, выслать пользователю… Подобным образом придётся постоянно пересылать весь набор данных от клиента серверу и обратно, причём много раз. Кроме того, что такая пересылка не всегда удобна, она ещё и пожирает траффик.

 

Механизм передачи данных между страницами без сессий.
Механизм передачи данных между страницами без сессий.Можно так же поступить иначе — использовать механизм сессий. Механизм этот работает следующим образом: данные, присланные пользователем, сервер сохраняет в отдельном файле — файле сессии. С содержимым этого файла и будет производиться вся работа по изменению данных. Клиенту же выдаётся «ключ сессии» (он же Session key, он же Sesseion ID) — уникальный указатель на файл, содержащий данные конкретно для этого пользователя. Теперь для того, чтобы получить все данные, касающиеся этого клиента, серверу необходимо знать лишь ключ сессии. Достоинством этого метода является удобство и скорость его использования.

 

Механизм передачи данных между страницами с помощью сессий.
Механизм передачи данных между страницами с помощью сессий.Вот и все основные методы интерфейса HttpServletRequest. Полный список методов читайте в документации к Java Servlet API.

Теперь об интерфейсе HttpServletRequest. Основное отличие классов, разделяющих данный интерфес, в том, что данные выводятся не сразу. Вначале происходит компановка всех данных в HTTP ответ. Ответ отправляется только после завершения работы HttpServlet.service().

И так, о методах:

void addHeader(String name, String value)
void addIntHeader(String name, int value)
void addDateHeader(String name, long date)

Методы добавляют в HTTP заголовок параметры. Последний метод устанавливает параметр «Date».

void addCookie(Cookie cookie)

Метод добавляет cookie в заголовок

boolean containsHeader(String name)

Позволяет узнать, содержит ли уже заголовок указанный параметр.

String encodeURL(String url)
String encodeRedirectURL(String url)

Первый метод кодирует символы с помощью замены %NN. Второй метод делает то же самое и вызывает

void sendRedirect(String location)

void setStatus(int sc)
void sendError(int sc)
void sendError(int sc, String msg)

Первый — устанавливает код возврата, вторые два — посылают сообщение об ошибке. В интерфейсе заданы следующие возможные ошибки для параметра sc, соответствующие кодам возврата протокола HTTP:

SC_CONTINUE — Status code (100)
SC_SWITCHING_PROTOCOLS — Status code (101)

SC_OK — Status code (200)
SC_CREATED — Status code (201)
SC_ACCEPTED — Status code (202)
SC_NON_AUTHORITATIVE_INFORMATION — Status code (203)
SC_NO_CONTENT — Status code (204)
SC_RESET_CONTENT — Status code (205)
SC_PARTIAL_CONTENT — Status code (206)

SC_MULTIPLE_CHOICES — Status code (300)
SC_MOVED_PERMANENTLY — Status code (301)
SC_MOVED_TEMPORARILY — Status code (302)
SC_SEE_OTHER — Status code (303)
SC_NOT_MODIFIED — Status code (304)
SC_USE_PROXY — Status code (305)

SC_BAD_REQUEST — Status code (400)
SC_UNAUTHORIZED — Status code (401)
SC_PAYMENT_REQUIRED — Status code (402)
SC_FORBIDDEN — Status code (403)
SC_NOT_FOUND — Status code (404)
SC_METHOD_NOT_ALLOWED — Status code (405)
SC_NOT_ACCEPTABLE — Status code (406)
SC_PROXY_AUTHENTICATION_REQUIRED — Status code (407)
SC_REQUEST_TIMEOUT — Status code (408)
SC_CONFLICT — Status code (409)
SC_GONE — Status code (410)
SC_LENGTH_REQUIRED — Status code (411)
SC_PRECONDITION_FAILED — Status code (412)
SC_REQUEST_ENTITY_TOO_LARGE — Status code (413)
SC_REQUEST_URI_TOO_LONG — Status code (414)
SC_UNSUPPORTED_MEDIA_TYPE — Status code (415)
SC_REQUESTED_RANGE_NOT_SATISFIABLE — Status code (416)
SC_EXPECTATION_FAILED — Status code (417)

SC_INTERNAL_SERVER_ERROR — Status code (500)
SC_NOT_IMPLEMENTED — Status code (501)
SC_BAD_GATEWAY — Status code (502)
SC_SERVICE_UNAVAILABLE — Status code (503)
SC_GATEWAY_TIMEOUT — Status code (504)
SC_HTTP_VERSION_NOT_SUPPORTED — Status code (505)

Вот и всё, что можно рассказать о HttpServletResponse

Использование сервлетов в WEB приложениях

Поговорим теперь об использовании сервлетов в WEB приложениях. Для этого я приведу два полезных примера, которые могут на практике пригодиться.

Первый пример показывает методы работы с HttpServlet и вывод содержимого HTML страницы в сжатом виде. По идее, HTML страница в ответе браузера выводится прямым текстом, но, чтобы сократить объём пересылаемых данных, можно использовать сжатие GZIP. Современные браузеры (по крайней мере браузеры 4 поколения и выше) поддерживают такой метод пересылки текстовой информации и выведут страницу так, как будто бы её и не сжимали.

Файл ZipServlet.java:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.zip.*;

// сервлет является наследником HttpServlet
public class ZipServlet extends HttpServlet
{
// функция обработки метода GET
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
// устанавливаем, что страничка является HTML документом
response.setContentType(«text/html»);

// берём параметр «Accept-Encoding» из HTTP заголовка
String encodings = request.getHeader(«Accept-Encoding»);

// берём параметр «encoding» — ранее заданная кодировка документа
String encodeFlag = request.getParameter(«encoding»);

// Куда будем выводить
PrintWriter out;

// если поле «Accept-Encoding» в запросе присутствует
if(encodings != null)
{
// и если это поле содержит значение «gzip», а кодировка ещё не была установлена,
if((encodings.indexOf(«gzip») != -1)&&!encodeFlag.equals(«none»))
{
// то то, куда будем выводит, будет за одним и сжимать текст с помощью GZIP
out = new PrintWriter(new GZIPOutputStream(response.getOutputStream()),
false);

// и устанавливаем флаг для браузера, что документ будет сжат
response.setHeader(«Content-Encoding», «gzip»);
}
else   // в противном случае выводить будем без сжатия
out = response.getWriter();
}
else    // в противном случае выводить будем без сжатия
out = response.getWriter();

out.println(«This a test!!!»); // пишем тело документа
out.close(); // и закрываем вывод.

//Всё, по завершению работы этой ф-ии, документ будет отправлен
}
}

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

Файл DoloadServlet.java:

import java.io.*;
import javax.servlet.*;

// программа реализует интерфейс Servlet
class DoloadServlet implements Servlet
{
ServletConfig config; // объект ServletConfig

public DoloadServlet() {} // ничего не делает

// при инициализации сохраняем config
public void init(ServletConfig config) throws ServletException
{this.config = config;}

// выдаёт сохранённый config
public ServletConfig getServletConfig() {return config;}

// информация о сервлете
public String getServletInfo() {return «DoloadServlet»;}

public void destroy() {} // ничего не делает

// обработка запроса
public void service(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException
{
// разбирать запрос мы не будем, просто срязу
// создаём HTTP заголовок:

String head = «HTTP/1.0 200 OK\n»+
+ «Server: DoloadServlet\n»
+ «Content-Type: text/html; charset=windows-1251\n»
+ «Connection: Keep-Alive\n»
+ «Content-Encoding: multipart/mixed\n»
+ «Transfer-Encoding: chunked»
+ «Pragma: no-cache\n\n»;

// теперь добавляем первоначальные данные
// для этого примера — 20 тэгов «<br>» с переносом строки
for(int i = 0; i < 20; i++) head = head + «<br>\n»;

// берём поток вывода
ServletOutputStream os = response.getOutputStream();

// пишем туда заголовок и первоначальные данные
os.print(head);

// отправляем всё записаное в буффер к клиенту
response.flushBuffer();

// начинаем добавлять новые строки:
// эти строки будут выглядеть следующим образом: номер строки, потом «<br>\n»
// каждая новая строка будет появляться раз в 3 секунды
int i = 0;
while(true)
{
// инкримент счётчика
i++;

// пишем строку
os.print(«»+i+»<br>\n»);

// сброс буффера
response.flushBuffer();

// примораживаем поток на 3 секунды
try {sleep(3000);}
catch(Exception e){}
}
}
}

Осталось сказать, что механизм сервлетов очень пластичен и позволяет творить такие вещи, которые могли бы потребовать написания отдельного WEB сервера (как, например, в случае сервлета докачки). Минусом работы сервлетов является низкая скорость первого запуска (сервлет просто компилируется JIT машиной), высокое поребление памяти и недостаток всех программ на Java — низкая скорость работы со строками. Последнее обстоятельство становится заметно при работе сервлетов, принимающих текстовые данные в POST запросах. POST запрос в HttpServlet размером в 50 кб при парсинге с помощью HttpServletRequest.getReader() может на пару минут парализавать работу сервера. То же относится и к другим программам на java.

Приведу два небольших примера:

// дана строка String text

// пример 1
// идёт работа со строкой с помощью операции «+» для String
String test1 = «»;
for(int i = 0; i < text.length(); i++)
test1 += text.charAt(i);

// пример 2
// идёт работа со строкой посредством буффера
char buf[] = new char[text.length()];
for(int i = 0; i < text.length(); i++)
buf[i] = text.charAt(i);
String sample2 = new String(buf);

Если взять небольшие строки — до 2-3 кб, то отличия в работе примеров несущественны, если же взять строку text размером хотя бы в 10 кб, то в первом случае программа будет работать со строкой значительно медленнее. Это является особенностью java и является проблемой реализации функций класса String. Так что если вы хотите написать быстрый сервлет, избегайте работу с длинными строками посредством класса String, используйте, к примеру, класс StringBuffer. Это предупреждение относится прежде всего к получению больших текстов из сети и к обработке локальных файлов (к примеру, в случае текстовой базы данных для гостевой книги при большом количестве сообщений).

Ещё одна проблема касается мультизадачности WEB системы. Не забывайте, что ваш сервлет могут одновременно запросить сразу несколько пользователей. Часто возникают проблемы синхронизации данных, обмену сведениями между разными вычислительными потоками одного и того же сервлета, а самая часто встречающаяся проблема — это проблема синхронного доступа к файлам и другим именованым ресурсам системы. К примеру, одна программа открыла файл на чтение, а другая тем временем пытается туда что-то писать. В результате вторая программа либо получает исключение, либо ждёт, пока файл освободится для записи. В связи с этим хочу обратить ваше внимание: не оставляйте за собою незакрытых потоков и закрывайте потоки, как только в них отпала необходимость. Поток, конечно, закроется позже автоматически, но это произойдёт только тогда, когда «мусорщик» до него доберётся, а меж тем вторая программа всё так же не будет иметь доступа к файлу на запись.

Дополнительно к мультизадачности хочу отметить, что с помощью методов «Object getAttribute(String name)» и «void setAttribute(String name, Object object)» интерфейса ServletContext вы можете обмениваться между сервлетами данными, в том числе и синхронизирующими.

Заключение

Статья эта не предполагалась такой длинной: она выросла из статьи для журнала «PAX-T», рассказывающей про HTTP протокол. Путём расширения статьи для сайта Developer Resource, она выросла в некое руководство по низкоуровневому программированию для WEB.

Когда писался данный опус, я постарался не просто перевести соответствующую документацию или дать набор приёмов для «правильного» программирования, а объяснить, что, где, как и почему применяется. Так же постарался максимально подробно осветить затронутые темы, не увлекаясь, впрочем, излишними деталями. О них вы можете прочитать в документах, ссылками на которые пестрит данная статья.

Не забывайте также, что Windows является торговой маркой компании Microsoft, а Java является торговой маркой Sun Microsystems.

Автор: Константин Андрюнин

Источник: http://www.javaportal.ru/java/articles/java_http_web/article06.html

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты