IT • archiv

rus / eng | Логин | Комментарий к колонке | Печать | Почта | Клуб




Колонки


HTTP протокол и работа с Web в Java. Работа с TCP/IP в Java, Сокеты

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

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

В 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: логическая структура соединений через сокеты.
Рисунок 1. 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, после чего ждёт входящих подключений. Приняв новое подключение, сервер передаёт его в отдельный вычислительный поток. В новом потоке сервер принимает от клиента данные, приписывает к ним порядковый номер подключения и отправляет данные обратно к клиенту.

логическая структура работы программ-примеров.
Рисунок 2. логическая структура работы программ-примеров.

Программа простого 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.




Справка | Условия Copyright © 1999 — 2010, IT • archiv.
В начало | Логин | Комментарий к колонке | Поиск | Почта