Как лучше передавать команды серверу java
Допустим есть сервер, имеющий 2 основных метода: вернуть значение какой-то переменной и обновить её значение. И есть клиент, который отправляет запрос и получает либо значение, либо уведомление о том, что данные получены. Как такое организовать? Что передавать по сети? Обычный текст? Сериализованные объекты с инструкциями серверу? PS: Лучшим ответом будет ссылка на какую-нибудь статью или мануал.
Отслеживать
задан 5 авг 2014 в 17:35
2,051 4 4 золотых знака 28 28 серебряных знаков 49 49 бронзовых знаков
Для описанной задачи подойдет все что угодно. Если появляются требования к производительности, безопасности и т.п., то нужно смотреть отдельно.
5 авг 2014 в 17:48
@avp, можете оформить этот комментарий как ответ?
Передаем файлы на Java с помощью ØMQ и JZMQ
Приветствую, мы являемся небольшой компанией единомышленников которые разрабатывает продукт предназначенный для управления данными, вне зависимости от их формата и метода хранения — ArkStore, в нашем блоге мы попытаемся поделится опытом, который мы накопили, в ходе, его уже почти двухлетней разработки. Первую статью я решил посветить IO слою и продукту под названием ØMQ (или ZeroMQ). Я попытаюсь рассказать как начать пользоваться ØMQ и как с его помощью можно передавать достаточно большой объем данных.
Почему ØMQ
В ходе разработки нашего продукта перед нами стояла задача — «как обеспечить надежную передачу большего объема данных?», при этом желательно что бы весь IO слой был полностью асинхронным, не поедал большое количество памяти и был достаточно простым. Изначально, так как вся наша архитектура была построена с использованием Akka, мы использовали Spray IO (или Akka IO). Но столкнулись с рядом проблем для которых не было адекватного решения, например обнаруженный мной баг заставлял нас создавать дополнительные Heartbeat сообщения или передавать большое количество служебной информации.
В итоге мы решили посмотреть в сторону брокеров сообщений. ActiveMQ, RabbitMQ и ØMQ. В принципе все брокеры решали поставленную перед нами задачу, но мы остановились на ØMQ. ActiveMQ показался слишком тяжеловесным, а RabbitMQ вносил мастер ноду в изначально распределенную архитектуру (без явного лидера).
- Request-Reply — самый простой распространенный шаблон, мы отправили запрос на сервер и получили ответ. Классическая клиент-серверная модель.
- Publish-Subscribe — в данном случае, сервер (publisher) периодически публикует информацию для своих подписчиков. Примером может служить, датчик, который постоянно публикует данные измерений для всех заинтересованных в них клиентов.
- Push-Pull — или Parallel Pipeline, позволяет нам проводить параллельные вычисления. Сервер равномерно рассылает сообщения содержащие работу (Push), машинам, которые могут произвести вычисления (workers), машины забирают эти сообщения (Pull), производят вычисления и отдают результаты клиенту (Push), заинтересованному в результатах вычислений (Pull). Картинка
В рамках данной статьи мы рассмотрим только первый шаблон (Request-Reply).
Собираем ØMQ и JZMQ
Перед тем как мы перейдем к коду, нам необходимо собрать сам ØMQ и библиотеки привязки (Java Bindings).
Linux
Для CentOS процесс будет выглядеть следующим образом (должен слабо отличатся для других *nix ОС).
Убедимся что у нас стоят все необходимые библиотеки:
yum install libtool autoconf automake gcc-c++ make e2fsprogs
Заберем и разархивируем последнюю стабильную версию библиотеки ØMQ с сайта разработчика (на момент написания 3.2.4).
wget http://download.zeromq.org/zeromq-3.2.4.tar.gz tar -xzvf zeromq-3.2.4.tar.gz cd zeromq-3.2.4
Собираем и устанавливаем ØMQ, библиотеки попадут в директорию /usr/local/lib, это понадобится нам в будущем.
./configure make sudo make install sudo ldconfig
После того как мы собрали ØMQ нам необходимо собрать JZMQ. Для этого выкачиваем последнюю версию с GIT репозитория (master или tag, последний tag на момент написания — 2.2.2).
wget https://github.com/zeromq/jzmq/archive/v2.2.2.zip unzip jzmq-2.2.2.zip cd jzmq-2.2.2 ./autogen.sh ./configure make sudo make install sudo ldconfig
Библиотеки так же попадут в директорию /usr/lib/local. Почему это важно? Дело в том, что для того что бы использовать нативные библиотеки, Java должна знать где мы можем их найти, для этого при запуске программы мы должны указать параметр java.library.path. Для этого есть несколько способов, мы можем указать его в момент запуска приложения -Djava.library.path=»/usr/lib/local», или установить его прямо во время работы программы. Мы так же можем использовать значение java.library.path, которое установлено по умолчанию. Что бы узнать, какие значение установлены по умолчанию, нужно выполнить следующую команду:
java -XshowSettings:properties
В моем случае это:
java.library.path = /usr/java/packages/lib/amd64 /usr/lib64 /lib64 /lib /usr/lib
Для того что бы Java смогла найти нативные библиотеки нам достаточно перенести наши библиотеки по одному из данных адресов или залинковать их. Какой подход выбрать, выбирать лично вам.
Что бы узнать куда были установлены библиотеки после make install, достаточно выполнить команды:
whereis libzmq whereis libjzmq
Windows
Собранные dll файлы для libzmq можно скачать с официального сайта, а тут можно найти руководство по сбору JZMQ под Windows. К сожалению, собрать библиотеки с помощью CMAKE у меня не получилось, пришлось собирать libzmq и jzmq с помощью Visual Studio 2013. При этом, важно что бы сами библиотеки были собраны под архитектуру соответствующую вашей JVM (32 или 64 битная),
Если libzmq.dll и jzmq.dll добавлены в PATH, то JVM должна их найти автоматически.
Программа
Ух, мы наконец смогли поставить и настроить ØMQ и JZMQ на нашем компьютере! Настало время пустить его в дело. В качестве примера, мы попробуем с помощью реализовать протокол передачи файлов описанный в руководстве и немного улучшить его.
- Протокол должен передавать данные асинхронно, не дожидаясь каждый раз ответа от клиента о получении данных.
- Сервер должен уметь работать с большим количеством клиентов (уметь идентифицировать клиента)
- При этом протокол должен аккуратно обращаться с памятью сервера, не держа большое количество данных в памяти, это важно как для клиента так и для сервера, например если клиент будет обрабатывать данные намного медленней чем их получает.
- Протокол должен поддерживать отмену передачи данных.
- Дать возможность клиенту выбирать размер кусков данных (chunks) которые он принимает.
- Поддерживать рестарт передачи данных (например если произошла ошибка и мы хотим возобновить передачу данных с определенного места, не передавая весь файл заново).
dd if=/dev/urandom of=testdata bs=1M count=1024
Замерим сколько времени ОС требуется для копирования данных. Данные цифры довольно приблизительны, но хотя бы мы будем иметь какую то точку сравнения.
echo 3 > /proc/sys/vm/drop_caches time cp testdata testdata2 real 0m7.745s user 0m0.011s sys 0m1.223s
Приступим к коду. Клиент:
package com.coldsnipe.example; import org.zeromq.ZFrame; import org.zeromq.ZMQ; public class ZMQClient implements Runnable < // Сколько сообщений мы отправим в "кредит" private static final int PIPELINE = 10; // Размер 1го куска данных, 250кб. private static final int CHUNK_SIZE = 250000; private final ZMQ.Context zmqContext; public ZMQClient(ZMQ.Context zmqContext) < this.zmqContext = zmqContext; >@Override public void run() < // Создаем ZMQ.Socket, в данном случае это DEALER socket try (ZMQ.Socket socket = zmqContext.socket(ZMQ.DEALER)) < // В случае если мы закрываем сокет но у нас есть сообщения // в очереди, мы ждем 1 секунды до того как скинем (drop) эти сообщения socket.setLinger(1000); // Соединяемся с сервером используя TCP протокол socket.connect("tcp://127.0.0.1:6000"); // Текущий кредит, сколько Chunks мы еще можем принять int credit = PIPELINE; // Количество переданных байт long total = 0; // Количество переданных кусков (Chunks) int chunks = 0; // Наш отступ от начала файла long offset = 0; while (!Thread.currentThread().isInterrupted()) < // Пока у нас есть "кредит", отправляем запросы на получение // кусков данных while (credit >0) < socket.sendMore("fetch"); socket.sendMore(Long.toString(offset)); socket.send(Integer.toString(CHUNK_SIZE)); offset += CHUNK_SIZE; credit--; >// Получаем фрейм данных от нашего сокета ZFrame zFrame = ZFrame.recvFrame(socket); // Если фрейм пустой, прерываем цикл if (zFrame == null) < break; >chunks++; credit++; int size = zFrame.getData().length; total += size; zFrame.destroy(); // Если размер куска меньше чем размер по умолчанию // значит мы получили последний кусок данных if (size < CHUNK_SIZE) < break; >> System.out.format("%d chunks received, %d bytes %n", chunks, total); > > >
- Команда — то что мы хотим от сервера, в данном случае команда всего одна, fetch — получить данные.
- Параметры команды (если присутствуют) — в случае с fetch это отступ от начала файла и размер куска данных.
package com.coldsnipe.example; import org.zeromq.ZFrame; import org.zeromq.ZMQ; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; public class ZMQServer implements Runnable < private final ZMQ.Context zmqContext; // Путь до файла который мы будем отдавать клиенту private final Path filePath; public ZMQServer(ZMQ.Context zmqContext, Path filePath) < this.zmqContext = zmqContext; this.filePath = filePath; >@Override public void run() < try < File file = filePath.toFile(); if (!file.exists()) < throw new RuntimeException("File does not exists: " + filePath); >// Открываем файл и сокет, Router сокет знает identity клиента, // таким образом мы можем различать клиенты между собой try (FileChannel fileChannel = FileChannel.open(filePath)) < try (ZMQ.Socket socket = zmqContext.socket(ZMQ.ROUTER)) < // Пытаемся поднять наш сервер на localhost на 6000 порту socket.bind("tcp://*:6000"); socket.setLinger(1000); while (!Thread.currentThread().isInterrupted()) < // Первый фрейм - всегда identity frame ZFrame identity = ZFrame.recvFrame(socket); assert identity != null; // Дальше идет команда String command = socket.recvStr(); if (command.equals("fetch")) < // В случае если это fetch, мы получаем offset и желаемый размер куска String offsetString = socket.recvStr(); long offset = Long.parseLong(offsetString); String chunkSizeString = socket.recvStr(); int chunkSize = Integer.parseInt(chunkSizeString); int currentChunkSize = chunkSize; // Проверяем если offset + размер куска не больше нашего файла // в случае если это так, мы уменьшаем размер куска от offset до конца файла if (file.length() < (offset + chunkSize)) < currentChunkSize = (int) (file.length() - offset); >if (currentChunkSize > 0) < ByteBuffer byteBuffer = ByteBuffer.allocate(currentChunkSize); fileChannel.read(byteBuffer, offset); byteBuffer.flip(); byte[] bytes = new byte[currentChunkSize]; byteBuffer.get(bytes); // Подготовляем фрейм с данными ZFrame frameToSend = new ZFrame(bytes); // Первым фреймом в ответе всегда должен быть identity // клиента, которому мы отсылаем данные identity.send(socket, ZFrame.MORE); frameToSend.send(socket, 0); >> > > > > catch (IOException e) < throw new RuntimeException(e); >> >
Наш сервер умеет передавать всего один файл (ссылку на который он получил при старте) и отвечать всего на одну команду — fetch. Он умеет различать клиентов, но клиенты могут получить только один единственный файл. Как это улучшить я напишу чуть ниже, а пока тест и результаты измерений.
package com.coldsnipe.example; import org.junit.Test; import org.zeromq.ZMQ; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; public class ZMQExampleTest < @Test public void testDataExchange() throws Exception < // Создаем ZMQ.Context, нам необходим всего 1! контекст на всю нашу программу // в скобках указывается количество потоков которое будет отведено контексту ZMQ.Context zmqContext = ZMQ.context(1); // Находим наши тестовые данные final URL fileUrl = ZMQExampleTest.class.getClassLoader().getResource("testdata"); assert fileUrl != null; Path filePath = Paths.get(fileUrl.toURI()); // Замеряем время и стартуем наш клиент не сервер, порядок старта не важен // клиент может быть запущен раньше сервера long startTime = System.nanoTime(); Thread clientThread = new Thread(new ZMQClient(zmqContext)); Thread serverThread = new Thread(new ZMQServer(zmqContext, filePath)); clientThread.start(); serverThread.start(); clientThread.join(); long estimatedTime = System.nanoTime() - startTime; float timeForTest = estimatedTime / 1000000000F; System.out.format("Elapsed time: %fs%n", timeForTest); // Не забываем остановить контекст, данная операция является блокирующей // и не будет завершена если у нас есть открытые сокеты. zmqContext.term(); >>
Наш тест запускает ZMQ.Context, стартует клиент и сервер, и замеряет время необходимо для передачи данных. О контексте хочется сказать отдельно. Именно контекст является скрытым дирижером который управляет нашими сокетами внутри процесса и решает как и когда отправить данные. Поэтому отсюда вытекает простое правило — один контекст на один процесс.
Запустим тест и посмотрим на результат.
4295 chunks received, 1073741824 bytes Elapsed time: 1.429522s
Чтение одного гигабайта заняло 1.42 секунды. Мне тяжело сказать насколько хороший это показатель, но в сравнении с тем же Spray IO, ØMQ отрабатывает на 30-40% быстрей, при этом нагрузка на IO близка к 100% (у Spray 85-90), нагрузка на CPU ниже почти на треть.
Улучшаем протокол
- get — сообщение о том что клиент хочет получить данные, должен содержать какие данные мы хотим получить
- end — сообщение посылаемое после того как клиент закончил принимать данные, свидетельствует о том что сервер может освободить ресурсы связанные с передаваемым файлом.
else if(command.equals("get")) < // ID данных String // Проверяем запрашивал ли данный клиент заголовок BigInteger identityString = new BigInteger(identity.getData()); DataBlob dataBlob = blobMap.get(identityString); if (dataBlob!= null)< // В случае если запрос уже был, освобождаем текущий DataBlob dataBlob.closeSource(); blobMap.remove(identityString); >// Получаем данные из хранилища DataBlob dataBlob = dataProvider.getBlob(id); if (dataBlob == null) < log.error("Received wrong get call on server socket, ID [<>]", id); > else < // Создаем новый заголовок, который затем енкодим и отправляем его клиенту DataHeader dataHeader = new DataHeader(id, dataBlob.getSize()); byte[] bytesToSend = FrameHeaderEncoder.encode(dataHeader); ZFrame frameToSend = new ZFrame(bytesToSend); // Кладем полученный DataBlob в коллекцию blobMap.put(new BigInteger(identity.getData()), dataBlob); // Отправляем заголовок клиенту identity.send(socket, ZFrame.MORE); frameToSend.send(socket, 0); >> else if (command.equals("end")) < BigInteger identityString = new BigInteger(identity.getData()); // Удаляем DataBlob и освобождаем данные DataBlob dataBlob = blobMap.remove(identityString); if (dataBlob != null) < dataBlob.closeSource(); >>
Для того что бы мы могли работать с множеством файлов, мы дополнили сервер новыми объектами.
public interface DataProvider < public DataBlob getBlob(String dataId); >public abstract class DataBlob < private final long size; private final String dataId; protected DataBlob(long size, String dataId) < this.size = size; this.dataId= dataId; >public abstract byte[] getData(long position, int length); public abstract void closeSource(); public long getSize() < return size; >public String getDataId() < return getDataId; >>
Класс реализующий DataProvider управляет получением данных, метод getBlob возвращает нам новый DataBlob который по сути является ссылкой на ресурс.
Реализация DataBlob для файла может выглядит следующим образом:
public class FileDataBlob extends DataBlob < private final FileChannel fileChannel; public FileDataBlob(long size, String dataId, Path filePath) < super(size, dataId); try < this.fileChannel = FileChannel.open(filePath); >catch (IOException e) < throw new DataBlobException(e); >> @Override public byte[] getData(long position, int length) < ByteBuffer byteBuffer = ByteBuffer.allocate(length); try < fileChannel.read(byteBuffer, position); byteBuffer.flip(); byte[] bytes = new byte[length]; byteBuffer.get(bytes); return bytes; >catch (IOException e) < throw new DataBlobException(e); >> @Override public void closeSource() < try < fileChannel.close(); >catch (IOException e) < throw new DataBlobException(e); >> >
Добавив эти два метода мы позволяем выбирать какие данные хочет получить клиент и информируем клиента о размере запрошенных данных. Мы можем и далее улучшать протокол, например добавить возможность пересылки множества файлов подряд, добавить Heartbeat для определения мертвых клиентов и освобождения ресурсов и т.д.
Заключение
В ходе данной статьи я хотел показать каким образом мы можем использовать ØMQ в Java. В данный момент в нашем проекте именно ØMQ является основным брокером сообщений, не только для файлов но и для метаданных, показывая довольно хорошие результаты (проблем, связанных именно с ним, пока не наблюдалось).
В следующих статьях я попытаюсь рассказать о других технологиях используемых в ArkStore, пока на очереди Akka и Semantic Web. Спасибо за внимание, надеюсь прочитанные было хоть кому то полезно!
- Блог компании Coldsnipe
- Java
Thread’ом Java не испортишь: Часть I — потоки

Многопоточность в Java была заложена с самых первых дней. Поэтому давайте кратко ознакомимся с тем, про что это — многопоточность. Возьмём за точку отсчёта официальный урок от Oracle: «Lesson: The «Hello World!» Application». Код нашего Hello World приложения немного изменим на следующий:
class HelloWorldApp < public static void main(String[] args) < System.out.println("Hello, " + args[0]); >>

args — это массив входных параметров, передаваемых при запуске программы. Сохраним данный код в файл с именем, которое совпадает с именем класса и с расширением .java . Скомпилируем при помощи утилиты javac: javac HelloWorldApp.java После этого вызовем наш код с каким-нибудь параметром, например, Roger: java HelloWorldApp Roger У нашего кода сейчас есть серьёзный изъян. Если не передать никакой аргумент (т.е. выполнить просто java HelloWorldApp), мы получим ошибку:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at HelloWorldApp.main(HelloWorldApp.java:3)
Возникло исключение (т.е. ошибка) в thread (в потоке) с именем main . Получается, в Java есть какие-то потоки? Отсюда начинается наш путь.
Java и потоки
Чтобы разобраться, что такое поток, надо понять, как происходит запуск Java приложения. Давайте изменим наш код следующим образом:
class HelloWorldApp < public static void main(String[] args) < while (true) < //Do nothing >> >

Теперь давайте скомпилируем это снова при помощи javac. Далее для удобства запустим наш Java код в отдельном окне. В Windows это можно сделать так: start java HelloWorldApp . Теперь при помощи утилиты jps посмотрим, какую информацию нам сообщит Java: Первое число — это PID или Process ID, идентификатор процесса. Что такое процесс?
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство.
При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Более подробно советую ознакомиться в статье: «https://habr.com/post/164487/». Процесс не может существовать без потоков, поэтому если существует процесс, в нём существует хотя бы один поток. Как же это происходит в Java? Когда мы запускаем Java программу, ее выполнение начинается с метода main . Мы как бы входим в программу, поэтому этот особый метод main называется точкой входа, или «entry point». Метод main всегда должен быть public static void , чтобы виртуальная машина Java (JVM) смогла начать выполнение нашей программы. Подробнее см. «Why is the Java main method static?». Получается, что java launcher (java.exe или javaw.exe) — это простое приложение (simple C application): оно загружает различные DLL, которые на самом деле являются JVM. Java launcher выполняет определённый набор Java Native Interface (JNI) вызовов. JNI — это механизм, соединяющий мир виртуальной машины Java и мир C++. Получается, что launcher — это не JVM, а её загрузчик. Он знает, какие правильные команды нужно выполнить, чтобы запустилась JVM. Знает, как организовать всё необходимое окружение при помощи JNI вызовов. В эту организацию окружения входит и создание главного потока, который обычно называется main . Чтобы нагляднее рассмотреть, какие живут потоки в java процессе, используем программу jvisualvm, которая входит в поставку JDK. Зная pid процесса, мы можем открыть данные сразу по нему: jvisualvm —openpid айдипроцесса
Интересно, что каждый поток имеет свою обособленную область в памяти, выделенной для процесса. Эту структуру памяти называют стеком. Стек состоит из фрэймов. Фрэйм — это точка вызова метода, execution point. Также фрэйм может быть представлен как StackTraceElement (см. Java API для StackTraceElement). Подробнее про память, выделяемую каждому потоку, можно прочитать тут. Если посмотреть на Java API и поискать там слово Thread, мы увидим, что есть класс java.lang.Thread. Именно этот класс представляет в Java поток, и с ним нам и предстоит работать. 
java.lang.Thread
- Не вызван метод Runtime.exit
- Все НЕ демон-потоки завершили свою работу (как без ошибок, так и с выбрасыванием исключений)
public static void main(String []args)
Группы позволяют упорядочить управление потоками и вести их учёт. Помимо групп, у потоков есть свой обработчик исключений. Взглянем на пример:
public static void main(String []args) < Thread th = Thread.currentThread(); th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() < @Override public void uncaughtException(Thread t, Throwable e) < System.out.println("Возникла ошибка: " + e.getMessage()); >>); System.out.println(2/0); >
Деление на ноль вызовет ошибку, которая будет перехвачена обработчиком. Если обработчик не указывать самому, отработает реализация обработчика по умолчанию, которая будет в StdError выводить стэк ошибки. Подробнее можно прочитать в обзоре http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/». Кроме того, у потока есть приоритет. Подробнее про приоритеты можно прочитать в статье «Java Thread Priority in Multithreading».
Создание потока
Как и сказано в документации, у нас 2 способа создать поток. Первый — создать своего наследника. Например:
public class HelloWorld < public static class MyThread extends Thread < @Override public void run() < System.out.println("Hello, World!"); >> public static void main(String []args) < Thread thread = new MyThread(); thread.start(); >>
Как видим, запуск задачи выполняется в методе run , а запуск потока в методе start . Не стоит их путать, т.к. если мы запустим метод run напрямую, никакой новый поток не будет запущен. Именно метод start просит JVM создать новый поток. Вариант с потомком от Thread плох уже тем, что мы в иерархию классов включаем Thread. Второй минус — мы начинаем нарушать принцип «Единственной ответственности» SOLID, т.к. наш класс становится одновременно ответственным и за управление потоком и за некоторую задачу, которая должна выполняться в этом потоке. Как же правильно? Ответ находится в том самом методе run , который мы переопределяем:
public void run() < if (target != null) < target.run(); >>
Здесь target — это некоторый java.lang.Runnable , который мы можем передать для Thread при создании экземпляра класса. Поэтому, мы можем сделать так:
public class HelloWorld < public static void main(String []args)< Runnable task = new Runnable() < public void run() < System.out.println("Hello, World!"); >>; Thread thread = new Thread(task); thread.start(); > >
А ещё Runnable является функциональным интерфейсом начиная с Java 1.8. Это позволяет писать код задач для потоков ещё красивее:
public static void main(String []args) < Runnable task = () ->< System.out.println("Hello, World!"); >; Thread thread = new Thread(task); thread.start(); >
Итого
Итак, надеюсь, из сего повестования понятно, что такое поток, как они существуют и какие базовые операции с ними можно выполнять. В следующей части стоит разобраться, как потоки взаимодействуют друг с другом и какой у них жизненный цикл. #Viacheslav
| Что еще почитать: |
|---|
| Thread’ом Java не испортишь: Часть II — синхронизация Thread’ом Java не испортишь: Часть III — взаимодействие Thread’ом Java не испортишь: Часть IV — Callable, Future и друзья Thread’ом Java не испортишь: Часть V — Executor, ThreadPool, Fork Join Thread’ом Java не испортишь: Часть VI — К барьеру! |
Как пересылать служебные команды java
Программы для работы с WEB.
Для того, чтобы получше понять как работать с HTTP, эта часть статьи будет посвящена написанию простой, но полезной утилиты для иследования WEB узлов, рассмотрения работы proxy-серверов, написанию простого кэширующего proxy и простейшего WEB сервера.
Простой HTTP клиент.
Начну же c написания простого HTTP клиента. Идея этой утилиты проста. Иногда, для исследования защиты сервера, очень удобно подсмотреть какой заголовок он выдаёт браузеру и какие куки он при этом присылает клиенту. Также эта утилита пригодиться, если требуется подменить поле «User-Agent» или послать такие cookie, которые сервер не записывал на ваш компьютер. Программа выполняет одно не очень хитрое действие: она читает заранее приготовленный файл, содержащий HTTP запрос, и отсылает его серверу, а ответ сервера, вместе с HTTP заголовком, записывает в другой файл, имя которого задано в параметрах инициализации.
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 сервер.
- сервер будет принимать заголовки длиною не более 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 из первой части статьи:
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». Файл может быть, допустим, таким:
test fileThis a test.
Теперь откройте браузер и наберите адрес «http://localhost» или «http://localhost/index.html». Страничка должна отобразиться.
Proxy серверы.
Proxy (proxy — заместитель, посредник) серверы (в просторечье — просто «прокси» или «прокси сервер») — это узловые станции интернета. Они отвечают за соединение различных сегментов интернета меджду собою, а так же могут выполнять несколько других полезных действий. Хотя функционально различные варианты прокси перекрывают друг друга, всё же можно выделить несколько их основных типов.
Первый тип прокси — это так называемый «шлюз». Как было сказанно в первой части статьи, IP адрес для каждой TCP/IP сети должен быть уникальным. Каждый IP в сети интернет тоже уникален, по этому возникает закономерный вопрос подключения частной локальной сети к сети интернет. Этот вопрос как раз и решают шлюзы (они же proxy-gate). Программа такого прокси устанавливается на одном из серверов внутренней сети, имеющий выход в Internet. Разберём принцип работы такого прокси.
- клиент открывает сокет локальной сети до прокси сервера («127.0.0.2:3128»)
- в открытый сокет клиент пишет HTTP запрос примерно следующего содержания:
GET http://www.devresource.org/ HTTP/1.1 Host: www.devresource.org
Для лучшего понимания того, что пошагово описано выше, прилагаю схему, иллюстрирующую процесс шлюзования запроса:
Схема работы шлюза.
Следующим типом прокси является «анонимный прокси» или анонимайзер. Принцип его работы схож с работой шлюза, но задача немного не та: задачей анонимайзера является скрыть IP адрес клиента. Этот прокси не пересылает запросы между разными TCP/IP сетями, он просто выступает посредником между клиентом и запрашиваемым хостом.
Анонимный прокси всё так же получает запрос от клиента, обрабатывает поле «Host», передаёт запрос серверу и возвращает его ответ. Единственное отличие в том, что и ServerSocket, и Socket до указанного хоста лежат в одной сети. Собственно, анонимный прокси является самым простым типом прокси-серверов.
Ещё одним типом прокси являются так называемые «Firewall» (firewall — огненная стена, в просторечье — файрвол). Это модули системы защиты компьютеров и локальных сетей. Для HTTP суть этих модулей сводится к тому, что они фильтруют нежелательный контент. Например рассмотрим принцип работы простейшего HTTP Firewall, отсекаюего загрузку любых не-текстовых документов и запрещающий запрос страничек, URL которых содержит в себе ключевые слова «sex», «chat» и т.д. У многих на работе стоят подобные файрволы (обычно они функционально совмещены с шлюзами). Зная, как они работают, можно попытаться обойти их.
- клиент открывает сокет локальной сети до прокси сервера и отправляет ему заголовок
- файрвол обрабатывает HTTP заголовок запроса: выделяет URL ресурса и сканирует его на наличие «запретных» слов.
- если слова найдены — возвращает клиенту ошибку типа «403 Forbidden» и завершает с ним соединение
- если URL ресурса «в порядке», то соединяется с указанным хостом и передаёт ему запрос, в котором подменяет метод запроса на «HEAD». Например так:
HEAD [URI] HTTP/1.1 Host: [host]
HTTP/1.1 200 OK Content-Type: image/gif Content-Length: [размер файла в байтах] [код картинки]
Рассмотрим ещё один тип прокси, называемый кэширующим (cache-proxy).
Что это значит кэширующий прокси? Это значит, что такой прокси сохраняет некоторые странички к себе в память, а потом, если от клиента придёт запрос на эту страничку, файл будет загружён не из сети, а из кэша — области памяти, куда прокси сохранил страничку.
Зачем это нужно? Данный принцип значительно сокращает траффик, ведь стоит одному клиенту обратиться, скажем, к «http://www.devresource.org/», как страничка окажется в кэше и для всех следующих клиентов, работающих через данный прокси и запрашивающих «http://www.devresource.org/», серверу не нужно будет снова загружать эту страничку из сети: достаточно будет просто достать её из кэша.
Какие странички нужно кэшировать, а какие — нет регламентируется следующими правилами: прежде всего, сохраняются только те странички, что были получены методом GET запроса. Кроме того, сохранять или не сохранять в кэш регламентируют такие поля HTTP, как «Pragma» и «Cache-Control». Встречаются эти поля, как вы помните, и в HTTP запросе и в HTTP ответе.
- «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 из первой части статьи:
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. HTTP протокол и работа с WEB» | © 2003, Konstantin Andrunin |
