четвер, 16 лютого 2012 р.

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

Современные фреймворки достаточно хорошо абстрагированы от низкоуровневых деталей собственной реализации. С одной стороны это хорошо - упрощает нам жизнь, но с другой стороны не дает нам лишнего повода узнать фундаментальные вещи. Это справедливо для огромного количества вопросов и областей информационных технологий, но в этом цикле статей речь пойдет о протоколе 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.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

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

    public static void main(String[] args) {
        /* Если аргументы отсутствуют, порт принимает значение по умолчанию */
        int port = DEFAULT_PORT;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        /* Установка кодировки консоли */
        String consoleEncoding = System.getProperty("consoleEncoding");
        if (consoleEncoding != null) {
            try {
                System.setOut(new PrintStream(System.out, true, consoleEncoding));
            } catch (java.io.UnsupportedEncodingException ex) {
                System.err.println("Unsupported encoding set for console: " 
                        + consoleEncoding);
            }
        }

        /* Создаем серверный сокет на полученном порту */
        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.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;

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

    public static void main(String[] args) throws UnsupportedEncodingException {
        /* Определяем хост сервера и порт */
        String host = DEFAULT_HOST;
        int port = DEFAULT_PORT;
        if (args.length > 0) {
            host = args[0];
        }
        if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

        /* Установка кодировки консоли */
        String consoleEncoding = System.getProperty("consoleEncoding");
        if (consoleEncoding != null) {
            try {
                System.setOut(new PrintStream(System.out, true, consoleEncoding));
            } catch (java.io.UnsupportedEncodingException e) {
                System.err.println("Unsupported encoding set for console: " 
                        + consoleEncoding);
            }
        }

        /*
         * Создаем сокет для полученной пары хост/порт
         */
        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, consoleEncoding));
        /*
         * Получаем поток вывода, через который будут передаваться сообщения
         * серверу
         */
        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://github.com/programming086/java_http

Немає коментарів:

Дописати коментар

HyperComments for Blogger

comments powered by HyperComments