Http в java. Часть первая - TCP.

четверг, 16 февраля 2012 г.

Современные фреймворки достаточно хорошо абстрагированы от низкоуровневых деталей собственной реализации. С одной стороны это хорошо - упрощает нам жизнь, но с другой стороны не дает нам лишнего повода узнать фундаментальные вещи. Это справедливо для огромного количества вопросов и областей информационных технологий, но в этом цикле статей речь пойдет о протоколе http (часто скрываемом за JSP, JSF и др.) и способах его использования в Java.

Http - протокол взаимодействия двух программ, клиента и сервера, которые могут быть запущены на совершенно разных и очень удаленных друг от друга машинах. Клиентом называют приложение, которое пользуется каким-то сервисом, предоставляемым другим приложением — Сервером, обычно на удаленном компьютере. Практически всегда клиент начинает исходящие соединения, а сервер ожидает входящих соединений (от клиентов), хотя бывают и исключения.

Первое, что надо понять - это способы взаимодействия клиента и сервера. Как они друг друга находят, и как обмениваются данными. Для этого давайте вспомним младшие курсы университета и освежим свои знания о сетевых протоколах. Если аббревиатуры OSI и TCP/IP не вводят вас в тупик, смело листайте дальше. Для тех же, кто старается не захламлять голову деталями, справедливо полагаясь на их доступность в Интернете, напоминаю:

 Модель OSI была предложена Международной организацией стандартов ISO (International Standards Organization) в 1984 году. Все сетевые функции в модели разделены на 7 уровней:
Распределение протоколов по уровням модели OSI
7 Прикладной напр., HTTP, SMTP, SNMP, FTP, Telnet, SSH, SCP, SMB, NFS, RTSP, BGP
6 Представления напр., XDR, AFP, TLS, SSL
5 Сеансовый напр., ISO 8327 / CCITT X.225, RPC, NetBIOS, PPTP, L2TP, ASP
4 Транспортный напр., TCP, UDP, SCTP, SPX, RTP, ATP, DCCP, GRE
3 Сетевой напр., IP, ICMP, IGMP, CLNP, OSPF, RIP, IPX, DDP, ARP, RARP
2 Канальный напр., Ethernet, Token ring, HDLC, PPP, X.25, Frame relay, ISDN, ATM, MPLS
1 Физический напр., электрические провода, радиосвязь, волоконно-оптические провода, Wi-Fi

При этом вышестоящие уровни выполняют более сложные, глобальные задачи, для чего используют в своих целях нижестоящие уровни, а также управляют ими. Цель нижестоящего уровня – предоставление услуг вышестоящему уровню, причем вышестоящему уровню не важны детали выполнения этих услуг. Нижестоящие уровни выполняют более простые и конкретные функции. В идеале каждый уровень взаимодействует только с теми, которые находятся рядом с ним (выше и ниже него). Верхний уровень соответствует прикладной задаче, работающему в данный момент приложению, нижний – непосредственной передаче сигналов по каналу связи.

На сегодняшний день основным стеком протоколов является TCP/IP. Этот стек протоколов появился до OSI и успел завоевать популярность.

Распределение протоколов по уровням модели TCP/IP
5 Прикладной
«7 уровень»
напр., HTTP, RTP, FTP, DNS
(RIP, работающий поверх UDP, и BGP, работающий поверх TCP, являются частью сетевого уровня)
4 Транспортный напр., TCP, UDP, SCTP, DCCP
(протоколы маршрутизации, подобные OSPF, что работают поверх IP, являются частью сетевого уровня)
3 Сетевой Для TCP/IP это IP (IP)
(вспомогательные протоколы, вроде ICMP и IGMP, работают поверх IP, но тоже относятся к сетевому уровню; протокол ARP является самостоятельным вспомогательным протоколом, работающим поверх физического уровня)
2 Канальный Ethernet, IEEE 802.11 Wireless Ethernet, SLIP, Token Ring, ATM и MPLS
1 Физический напр., физическая среда и принципы кодирования информации, T1, E1

В обеих моделях http находится на прикладном уровне и использует для собственной реализации протоколы более низких уровней. Рассмотреть все уровни - задача далеко выходящая за рамки одной статьи. Поэтому здесь я ограничусь лишь кратким описанием одного из протоколов транспортного уровня, а именно TCP, на который непосредственно опирается http.

Согласно протоколу IP, каждый узел в сети имеет свой IP-адрес, состоящий из 4х байт и обычно записываемый как n.n.n.n Каждый узел напрямую «видит» только узлы в своей подсети.

TCP протокол базируется на IP для доставки пакетов, но добавляет две важные вещи:
  • установление соединения — это позволяет ему, гарантировать доставку пакетов
  • порты — для обмена пакетами между приложениями, а не просто узлами

Таким образом, для идентификации машины в сети используется протокол IP и 4х байтный уникальный идентификатор машины - ip адрес. Далее, для идентификации приложения, TCP добавляет понятие порта. Порт - это целое число от 1 до 65535 указывающее, какому приложению предназначается пакет. В TCP пакетах указываются порт источника(клиента) и порт назначения(сервера).

Сервер при запуске сообщает операционной системе, что хотел бы «занять» определенный порт (или несколько портов). После этого все пакеты, приходящие на компьютер к этому порту, ОС будет передавать этому серверу. Говорят, что сервер «слушает» этот порт.

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

Еще одним, очень важным понятием в сетевом взаимодействии, является Сокет (socket - генздо, панель). На самом деле, это не новое понятие, а совокупность старых: ip-адреса и порта.

Вики призывает различать клиентские и серверные сокеты:
Следует различать клиентские и серверные сокеты. Клиентские сокеты грубо можно сравнить с оконечными аппаратами телефонной сети, а серверные — с коммутаторами. Клиентское приложение (например, браузер) использует только клиентские сокеты, а серверное (например, веб-сервер, которому браузер посылает запросы) — как клиентские, так и серверные сокеты.

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

Чтож, пришла пора попытаться воспользоваться знаниями и перейти от слов к джаве =)

Ключевыми классами для реализации взаимодействия программ по TCP являются java.net.Socket и java.net.ServerSocket.

  • java.net.Socket- этот класс реализует клиентские сокеты. Сокет представляет собой конечную точку для связи между двумя машинами.
  • java.net.ServerSocket - этот класс реализует серверный сокет. Серверный сокет ожидает запросы, приходящие по сети от клиентов и может, при необходимости отправлять ответ.
Интерфейсы этих классов достаточно богаты, но ключевая функциональность скрыта за небольшим числом методов. Способы использования этих классов проще показать на примере, чем давать комментарии к их методам.

Ниже приведены коды простейших TCP сервера и клиента. Сервер транслирует на консоль все, что ему приходит от клиента.

Сервер:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Слушает указанный в первом параметре порт и выводит все приходящие на него
 * сообщения на консоль.
 */
public class TcpServer {

   public static void main(String[] args) {
      /* Если аргументы отсутствуют, порт принимае значение поумолчанию */
      int port = DEFAULT_PORT;
      if (args.length > 0) {
         port = Integer.parseInt(args[0]);
      }
      /* Создаем серверный сокет на полученном порту */
      ServerSocket serverSocket = null;
      try {
         serverSocket = new ServerSocket(port);
      } catch (IOException e) {
         System.out.println("Порт занят: " + port);
         System.exit(-1);
      }
      /*
       * Если порт был свободен и сокет был успешно создан, можно переходить к
       * следующему шагу - ожиданию клинта
       */
      Socket clientSocket = null;
      try {
         clientSocket = serverSocket.accept();
      } catch (IOException e) {
         System.out.println("Ошибка при подключении к порту: " + port);
         System.exit(-1);
      }
      /*
       * Теперь можно получить поток ввода, в который помещаются сообщения от
       * клиента
       */
      InputStream in = null;
      try {
         in = clientSocket.getInputStream();
      } catch (IOException e) {
         System.out.println("Не удалось получить поток ввода.");
         System.exit(-1);
      }
      /*
       * В этой реализации сервер будет без конца читать поток и выводить его
       * содержимое на консоль
       */
      BufferedReader reader = new BufferedReader(new InputStreamReader(in));
      String ln = null;
      try {
         while ((ln = reader.readLine()) != null) {
            System.out.println(ln);
            System.out.flush();
         }
      } catch (IOException e) {
         System.out.println("Ошибка при чтении сообщения.");
         System.exit(-1);
      }
   }

   private static final int DEFAULT_PORT = 9999;
}

Клиент:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * Подключается к серверу и передает ему сообщения вводимые пользователем в
 * консоли. Хост сервера указвается первым аргументом, порт вторым. При
 * отсутствии аргументов в качестве адреса порта принимается localhost:9999
 */
public class TcpClient {

   public static void main(String[] args) {
      /* Определяем хост сервера и порт */
      String host = DEFAULT_HOST;
      int port = DEFAULT_PORT;
      if (args.length > 0) {
         host = args[0];
      }
      if (args.length > 1) {
         port = Integer.parseInt(args[1]);
      }
      /*
       * Создаем сокет для полученной пары хост/порт
       */
      Socket socket = null;
      try {
         socket = new Socket(host, port);
      } catch (UnknownHostException e) {
         System.out.println("Неизвестный хост: " + host);
         System.exit(-1);
      } catch (IOException e) {
         System.out.println("Ошибка ввода/вывода при создании сокета " + host
               + ":" + port);
         System.exit(-1);
      }
      /*
       * Для удобства обернем стандартный поток ввода в BufferedReader
       */
      BufferedReader reader = new BufferedReader(new InputStreamReader(
            System.in));
      /*
       * Получаем поток вывода, через который будут передаваться сообщения
       * серверу
       */
      OutputStream out = null;
      try {
         out = socket.getOutputStream();
      } catch (IOException e) {
         System.out.println("Не удалось получить поток вывода.");
         System.exit(-1);
      }
      /*
       * Все вводимые пользователем сообщения будем транслировать в поток вывода
       * созданного сокета
       */
      BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
      String ln = null;
      try {
         while ((ln = reader.readLine()) != null) {
            writer.write(ln + "\n");
            writer.flush();
         }
      } catch (IOException e) {
         System.out.println("Ошибка при записи сообщения.");
         System.exit(-1);
      }
   }

   private static final String DEFAULT_HOST = "localhost";
   private static final int DEFAULT_PORT = 9999;
}


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

На мой взгляд, понимание TCP является минимальной базой для понимания HTTP. В следующей части я постараюсь дать минимальное представление непосредственно о протоколе Http и стандартном API в Java для его реализации.

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

12 комментариев :

  1. улыбнула ссылка на статью в википедии "электрический провод"...ах да, это первая статья, которую дочитал до конца

    ОтветитьУдалить
  2. Было бы интересно увидеть реализацию такого, чтобы на веб-странице например можно было строить график функции синуса (или другой функции) в определенном промежутке (польователь задает начало и конец отрезка).

    ОтветитьУдалить
    Ответы
    1. Подобные сервисы давно существуют. Вот пример: http://school35.ucoz.ru/grapher/grapher_e.htm

      Удалить
  3. ну ты капитан-очевидность)))а вот как его замутить, и чтоб работал, вот в чем загадка, по крайней мере для меня)))я как-то писал на php программки вот и заинтересовался

    ОтветитьУдалить
    Ответы
    1. Да, я замечаю за собой периодически =) Вообще, в простейшей реализации ничего сложного быть не должно. Забираем аргументы у пользователя, на сервере строим картинку с графиком исходя из аргументов, возвращаем ее пользователю. С сегодняшними возможностями html5, вероятно(веб не моя специализация), и сервер подключать к решению задачи не придется.

      Удалить
    2. Было бы круто, если ты напишешь об этом в блоге. Заодно контент у тебя прибавиться)))

      Удалить
    3. у меня пока есть идеи для статей и нет свободного времени. Возможно, когда реализуются первые и появится последнее... Но ведь ты все равно ждешь код на php, а я могу предложить только java.

      Удалить
    4. Меня интересует реализация идеи, пусть хоть и на java)))

      Удалить
    5. Написание кода, подключение модуля к веб-странице и его работа

      Удалить
  4. спасибо, очень доходчиво написано

    ОтветитьУдалить
  5. Добрый день.
    А ведь потоки надо закрывать?

    ОтветитьУдалить
  6. Большое спасибо. Пока не разобрался, но скопировал и работает

    ОтветитьУдалить

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

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