Http в Java. Часть вторая - HTTP.

пятница, 24 февраля 2012 г.
 В предыдущей статье, был поверхностно рассмотрен стек TCP/IP и способы его использования в java. Это очень важный фундамент, который позволит перейти к изучению непосредственно http.

HTTP (HyperText Transfer Protocol - протокол передачи гипертекста)честно следует своему названию, и заключается в спецификации обмена сообщениями определенного текстового формата. Клиент и сервер обмениваются текстовыми сообщениями состоящими из заголовка сообщения и его тела. В заголовке указывается необходимая для взаимодействия информация.

Http заголовок запроса

Чтобы обратиться к серверу, клиент должен послать сообщение, которое в простейшем случае будет выглядеть следующим образом:
GET HTTP/1.1
В этом сообщении указана команда (get) и версия протокола (1.1), которую поддерживает клиент. Веб-сервер, исходя из этой информации, формирует ответное сообщение и отправляет его клиенту.

И так, у нас есть текстовое сообщение, которое необходимо передать программе, возможно запущенной на совершенно иной, удаленной машине. Благодаря протоколу TCP/IP эта задача кажется элементарной, для ее реализации осталось лишь узнать адрес целевого сервера. Для этого в протоколе http существует специальное поле: Host
GET HTTP/1.1
Host: www.w3.org:80
В нем указывается адрес запрашиваемого сервера и, при необходимости, через двоеточие порт. Чаще всего порт опускается, так как распространенная практика заключается в использовании порта 80. В приведенном ранее простейшем случае подразумевается локальная машина и порт 80.

На самом деле, спецификация http предусматривает достаточно много полей заголовка сообщения отправляемого клиентом (чаще всего браузером). Заголовок редко ограничивается парой строк и чаще выглядит примерно так:
GET http://www.w3.org/standards/webdesign/htmlcss HTTP/1.1
Host: www.w3.org
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Referer: http://localhost/
Accept-Language: ru
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Proxy-Connection: Keep-Alive


Обратите внимание, что символом конца заголовка являются два, идущих подряд, символа конца строки: "\n\n" (так же допускается "\n\r\n\r").

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

Строка запроса (Request-Line)

Все начинается с первой, обязательной строки запроса (Request-Line). Она должна содержать название команды/метода (Method), полный путь запрашиваемого ресурса(Request-URI), который может и отсутствовать, и версию протокола, поддерживаемую клиентом:
Request-Line   =  Method SP Request-URI SP HTTP-Version CRLF
Метод указывает действие, которое должен выполнить сервер с указанным ресурсом. В протоколе определены следующие методы:
  • OPTIONS - Используется для определения возможностей веб-сервера или параметров соединения для конкретного ресурса.
  • GET - Используется для запроса содержимого указанного ресурса. С помощью метода GET можно также начать какой-либо процесс.
  • HEAD - Аналогичен методу GET, за исключением того, что в ответе сервера отсутствует тело. Может использоваться для проверки доступности сервера.
  • POST - Применяется для передачи пользовательских данных заданному ресурсу. При этом передаваемые данные включаются в тело запроса. Так же, с помощью этого метода можно загружать файлы на сервер.
  • PUT - Применяется для загрузки содержимого запроса на указанный в запросе адрес. Если по заданному адресу не существовало ресурса, то сервер создаёт его и возвращает статус 201 (Created). Если же был изменён ресурс, то сервер возвращает 200 (Ok) или 204 (No Content).
  • DELETE - Команда на удаление указанного ресурса. Если это действие запрещено, возвращается код 403 (Forbidden).
  • TRACE - Возвращает полученный запрос так, что клиент может увидеть, какую информацию промежуточные серверы добавляют или изменяют в запросе.
  • CONNECT - Преобразует соединение запроса в прозрачный TCP/IP туннель, обычно чтобы содействовать установлению защищенного SSL соединения через не шифрованный прокси.
 Полный список методов с их описанием можно увидеть здесь.

Адрес сервера (Host)

Host: www.w3.org
Эта строка уже упоминалась. В ней указывается адрес сервера и, при необходимости, порт.

Поддерживаемые типы файлов (Accept)

Содержит перечень MIME типов файлов, которые может обработать клиент:
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Здесь * обозначает все множество допустимых значений. Например, image/* говорит о том, что клиент может обработать любое изображение. В некоторых случаях этот параметр помогает серверу сгенерировать более корректный ответ(переконвертировать изображение в поддерживаемый формат).

Адрес перехода(Referer)

Указывает адрес, с которого был выполнен переход.
Referer: http://localhost/

Поддерживаемый язык (Accept-Language)

Accept-Language: ru  
Помогает серверу сгенерировать ответ на родном, для клиента, языке. Магии не существует, поэтому сервер должен уметь предоставлять запрашиваемое содержимое на указанном языке. Чудом оно, к сожалению, не переведется.

Пользовательский клиент (User-Agent)

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; MyIE2; .NET CLR 1.0.3705)
Раскрывает информацию о клиенте, от которого выполняется запрос. Именно из этой строки получают информацию кривые сайты, заточенные под "недобраузер" IE и отказывающиеся корректно работать где бы то ни было еще :) Так же, здесь может быть указана версия операционной системы и некоторая дополнительная информация.

"Печеньки" (Cookie)

Cookie: param1=value1; param2=value2
Не смотря на забавный перевод термина, Cookie (в интернетах просто "куки") очень важная технология, избавляющая нас от постоянного заполнения форм авторизации и выполняющая еще много всевозможных полезных функций. Вообще, куки заслуживают отдельного поста... Возможно когда-нибудь, а пока добро пожаловать за подробностями на вики.

Диапазон(Range)

Range-Unit: 2048 | 1024
Это очень интересный параметр, указывающий серверу какую часть файла в байтах необходимо передать клиенту. Первое число указывает байт, с которого необходимо начать передачу, а второе - количество передаваемых байт. Благодаря этому параметру мы можем останавливать скачку файлов и продолжать ее даже после перезапуска клиента закачки(или браузера).

Эта статья не претендует на исчерпывающее изложение деталей http протокола. Наиболее подробную информацию о нем вы можете найти  по адресу: RFC 2616.

Простейший Http клиент на Java

Пришло время все "потрогать собственными руками". Для этого я предлагаю простейшую реализацию http клиента, который, следуя unix-way (о нем в ближайшей статье), будет получать на вход содержимое заголовка сообщения и отправлять его на сервер.




import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.text.ParseException;

public class HttpClient {

 public static void main(String[] args) {
  try {
   String header = null;
   if (args.length == 0) {
    header = readHeader(System.in);
   } else {
    FileInputStream fis = new FileInputStream(args[0]);
    header = readHeader(fis);
    fis.close();
   }
   System.out.println("Заголовок: \n" + header);
   /* Запрос отправляется на сервер */
   String answer = sendRequest(header);
   /* Ответ выводится на консоль */
   System.out.println("Ответ от сервера: \n");
   System.out.write(answer.getBytes());
  } catch (Exception e) {
   System.err.println(e.getMessage());
   e.getCause().printStackTrace();
  }
 }

 /**
  * Читает поток и возвращает его содержимое в виде строки.
  */
 public static String readHeader(InputStream strm) throws IOException {
  byte[] buff = new byte[64 * 1024];
  int length = strm.read(buff);
  String res = new String(buff, 0, length);
  return res;
 }

 /**
  * Отправляет запрос в соответствии с Http заголовком.
  * 
  * @return ответ от сервера.
  */
 public static String sendRequest(String httpHeader) throws Exception {
  /* Из http заголовка берется арес сервера */
  String host = null;
  int port = 0;
  try {
   host = getHost(httpHeader);
   port = getPort(host);
   host = getHostWithoutPort(host);
  } catch (Exception e) {
   throw new Exception("Не удалось получить адрес сервера.", e);
  }
  /* Отправляется запрос на сервер */
  Socket socket = null;
  try {
   socket = new Socket(host, port);
   System.out.println("Создан сокет: " + host + " port:" + port);
   socket.getOutputStream().write(httpHeader.getBytes());
   System.out.println("Заголовок отправлен. \n");
  } catch (Exception e) {
   throw new Exception("Ошибка при отправке запроса: "
     + e.getMessage(), e);
  }
  /* Ответ от сервера записывается в результирующую строку */
  String res = null;
  try {
   InputStreamReader isr = new InputStreamReader(socket
     .getInputStream());
   BufferedReader bfr = new BufferedReader(isr);
   StringBuffer sbf = new StringBuffer();
   int ch = bfr.read();
   while (ch != -1) {
    sbf.append((char) ch);
    ch = bfr.read();
   }
   res = sbf.toString();
  } catch (Exception e) {
   throw new Exception("Ошибка при чтении ответа от сервера.", e);
  }
  socket.close();
  return res;
 }

 /**
  * Возвращает имя хоста (при наличии порта, с портом) из http заголовка.
  */
 private static String getHost(String header) throws ParseException {
  final String host = "Host: ";
  final String normalEnd = "\n";
  final String msEnd = "\r\n";

  int s = header.indexOf(host, 0);
  if (s < 0) {
   return "localhost";
  }
  s += host.length();
  int e = header.indexOf(normalEnd, s);
  e = (e > 0) ? e : header.indexOf(msEnd, s);
  if (e < 0) {
   throw new ParseException(
     "В заголовке запроса не найдено " +
     "закрывающих символов после пункта Host.",
     0);
  }
  String res = header.substring(s, e).trim();
  return res;
 }

 /**
  * Возвращает номер порта.
  */
 private static int getPort(String hostWithPort) {
  int port = hostWithPort.indexOf(":", 0);
  port = (port < 0) ? 80 : Integer.parseInt(hostWithPort
    .substring(port + 1));
  return port;
 }

 /**
  * Возвращает имя хоста без порта.
  */
 private static String getHostWithoutPort(String hostWithPort) {
  int portPosition = hostWithPort.indexOf(":", 0);
  if (portPosition < 0) {
   return hostWithPort;
  } else {
   return hostWithPort.substring(0, portPosition);
  }
 }
}

Сохраняем исходный код в HttpClient.java, компилируем его
$> javac HttpClient.java
Создаем текстовый файл Header.txt и сохраняем в нем пример многострочного заголовка приведенного выше, заменив метод Get на Head. Напоминаю, в результате метода Get сервер вернет полное сообщение - и заголовок и тело. Но сейчас для нас тело не представляет особенного интереса т.к. представляет собой Html разметку страницы. Гораздо интереснее взглянуть на заголовок ответа. Именно для этого и существует метод Head. Он указывает серверу, что надо вернуть только заголовок сообщения и опустить его тело.
HEAD http://www.w3.org/standards/webdesign/htmlcss HTTP/1.1
Host: www.w3.org
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Referer: http://localhost/
Accept-Language: ru
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Proxy-Connection: Keep-Alive
Теперь передадим его содержимое в нашу программу для отправки на сервер:
$> cat Header.txt | java HttpClient
*в Windows аналогом команды cat является команда type

Http заголовок ответа

На момент статьи ответ сервера был следующим:
HTTP/1.0 200 OK
Date: Fri, 24 Feb 2012 06:56:41 GMT
Server: Apache/2
Content-Location: htmlcss.html
Vary: negotiate
TCN: choice
Last-Modified: Fri, 24 Feb 2012 03:02:02 GMT
ETag: "6063-4b9acfc1b3e80;4a8c5934a8200"
Accept-Ranges: bytes
Content-Length: 24675
Cache-Control: max-age=21600
Expires: Fri, 24 Feb 2012 12:56:41 GMT
P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml"
Content-Type: text/html; charset=utf-8

Список полей заголовка ответного сообщения велик и целиком его можно по прежнему увидеть здесь. Но для общего развития разберем пару параметров, что встретились в нашем примере.

И снова обязательной является только первая строка (строка состояния). В ней указывается версия протокола, трехзначный код ответа и его расшифровка:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF 
Все коды ответов делятся на пять категорий. Принадлежность к одной из категорий указывается первой цифрой кода:
  • 1xx: Informational(Информационные) - Запрос получен. Продолжается процесс.
  • 2xx: Success(Успешные) - Запрос был успешно получен, распознан и принят на обработку.
  • 3xx: Redirection(Коды перенаправления) - Необходимы дополнительные действия для выполнения запроса. Чаще всего, речь идет о перенаправлении на ресурс, указанный в поле Location.
  • 4xx: Client Error(Коды ошибок клиента) - Запрос не может быть выполнен. Чаще всего по причине некорректного синтаксиса.
  • 5xx: Server Error(Коды ошибок сервера) - Сервер не в состоянии правильно выполнить запрос. Такие ошибки часто связаны с ограничениями прав.
Вот не полный список возможных кодов ответа:
  • "100" Continue(Продолжение) - Сервер оповещает клиента о том, что часть его сообщения принята и ему следует продолжать запрос.
  • "200" OK(ОК он и Африке ОК) - Запрос выполнен успешно. Информация в ответе зависит от типа запроса POST, GET или HEAD.
  • "201" Created(Создано) - Запрос выполнен успешно. Ресурс создан. Местоположение созданного ресурса указывается в поле Location.
  • "202" Accepted (Принято) - Запрос был принят на обработку, но обработка еще не завершена. Сервер оповещает клиента о том, что ему не обязательно дожидаться окончательной передачи сообщения, так как процесс обработки может затянуться надолго. Типовой случай - передача файла.
  • "300" Multiple Choices(Множественный выбор) - Говорит о том, что по указанному URI сервер может предоставить несколько вариантов ресурса в зависимости от языка, MIME типа или другому признаку. Выбор предлагается сделать клиенту.
  • "400" Bad Request(Плохой запрос) - Признак синтаксической ошибки в запросе. (Чтобы воспроизвести,отправьте запрос, содержащий русские буквы, например напишите метод в транслите).
  • "404" Not Found(Не найдено) - Легендарная ошибка, с которой сталкивался каждый. Возникает по причине того, что сервер понял запрос, но не смог найти указанный ресурс. Чаще всего возникает по причине опечаток в URL.
  • "405" Method Not Allowed(Метод не поддерживается) - Говорит о недопустимости запрашиваемого действия к ресурсу. (Чтобы воспроизвести, попробуйте отправить запрос DELETE к, практически любому ресурсу).
  • "418" I'm a teapot (Я - чайник) - Возвращается при попытке заварить кофе в заварном чайнике. Серверу следует вернуть короткий и жёсткий ответ. (Часть протокола гипертекстового контроля кофеварками).
  • "500" Internal Server Error(Внутренняя ошибка сервера) - Сервер столкнулся с непредвиденными проблемами, не позволяющими ему выполнить запрос.
  • "501" Not Implemented(Не реализовано) - Сервер не поддерживает возможностей указанных в запросе. (Редкие серверы могут варить кофе. Если в методе указать BREW, то скорее всего именно этот ответ вы и получите).
  • "503" Service Unavailable(Сервер недоступен) - Сервер временно не в состоянии обрабатывать запросы.
  • "505" HTTP Version not supported(Указанная версия протокола Http не поддерживается) - название говорит само за себя.
Далее идет параметр указывающий дату.
Date: Fri, 24 Feb 2012 06:56:41 GMT
Это либо текущая дата (если документ динамический), либо дата создания отправляемого файла. Дата представлена в формате GMT.

Затем указывается сервер
Server: Apache/2

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


Следующий параметр указывает длину  отправленного сервером документа
Content-Length: 24675

Content-Type: text/html; charset=utf-8
Параметр указывающий MIME тип пересылаемого документа.

Подытожим

Эта статья не раскрыла новых тонкостей Java API для работы с http, но должна была привнести ясность в понимание того, чем же на самом деле является http: это не более чем договоренность о формате сообщений пересылаемых между сервером и клиентом. Способ их доставки играет отнюдь не ключевую роль. Вместо TCP/IP с задачей реализации http может справиться и Почта России, и почтовые голуби. Было бы желание! =)

UPD:  теперь доступен репозиторий, для  цикла статей об http в java:  https://bitbucket.org/dok/http


Что еще почитать:

  1. Википедия - Http
  2. RFC 2616
  3. RFC 2616 (перевод)
  4. Java. HTTP протокол и работа с WEB
  5. Описание протокола HTTP

1 комментарий :

Ваше мнение мне искренне интересно. Смелее!

Технологии Blogger.