пʼятницю, 29 червня 2012 р.

Http в java. Часть 2.5 - Простой web server

Предыдущая статья оказалась достаточно объемной и в то же время не законченной. Привести пример HTTP клиента и не показать простейшего HTTP сервера, это как сказать "а" и забыть про "б". Пришло время все исправить.

Представление о том, как получить сообщение от клиента и отправить ему ответ, вы можете почерпнуть из первой статьи цикла. Чтобы развить пример tcp сервера и вырастить из него http сервер, надо подружить его с http протоколом. Естественно, поддержка всего протокола сервером - задача не самая простая и очень далеко выходящая за рамки одной статьи. Поэтому приводимая реализация веб-сервера будет ограничена возможностью отдавать точно указанный контент и выводить содержимое запроса в консоль. Практическое применение такому серверу найти не просто, но для экспериментов, при изучении http и особенно ajax, он может пригодиться.

Реализация веб-сервера будет разбита на две части: первая (класс HttpServer) будет отвечать за прием сообщений от клиентов, вторая (класс ClientSession) за их обработку.

Код класса HttpServer представлен ниже:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Обрабатывает запросы от клиентов, возвращая файлы, указанные в url-path или
 * ответ с кодом 404, если такой файл не найден.
 *
 * @author brovko_rs
 */


public class HttpServer {

    /**
     * Первым аргументом может идти номер порта.
     */
    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);
            System.out.println("Server started on port: "
                    + serverSocket.getLocalPort() + "\n");
        } catch (IOException e) {
            System.out.println("Port " + port + " is blocked.");
            System.exit(-1);
        }
        /*
         * Если порт был свободен и сокет был успешно создан, можно переходить к
         * следующему шагу - ожиданию клинтов
         */
        while (true) {
            try {
                Socket clientSocket = serverSocket.accept();
                /* Для обработки запроса от каждого клиента создается
                 * отдельный объект и отдельный поток */
                ClientSession session = new ClientSession(clientSocket);
                new Thread(session).start();
            } catch (IOException e) {
                System.out.println("Failed to establish connection.");
                System.out.println(e.getMessage());
                System.exit(-1);
            }
        }
    }

    private static final int DEFAULT_PORT = 9999;
}
Приведенный код не должен вызвать затруднений. В нем создается серверный сокет, и при каждом новом подключении клиента его обработка делегируется очередному объекту ClientSession.

Полный код  ClientSession приводится ниже, после идут небольшие пояснения к нему:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Date;

/**
 * Обрабатывает запрос клиента.
 *
 * @author brovko_rs
 */


public class ClientSession implements Runnable {

    @Override
    public void run() {
        try {
            /* Получаем заголовок сообщения от клиента */
            String header = readHeader();
            System.out.println(header + "\n");
            /* Получаем из заголовка указатель на интересующий ресурс */
            String url = getURIFromHeader(header);
            System.out.println("Resource: " + url + "\n");
            /* Отправляем содержимое ресурса клиенту */
            int code = send(url);
            System.out.println("Result code: " + code + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public ClientSession(Socket socket) throws IOException {
        this.socket = socket;
        initialize();
    }

    private void initialize() throws IOException {
        /* Получаем поток ввода, в который помещаются сообщения от клиента */
        in = socket.getInputStream();
        /* Получаем поток вывода, для отправки сообщений клиенту */
        out = socket.getOutputStream();
    }

    /**
     * Считывает заголовок сообщения от клиента.
     *
     * @return строка с заголовком сообщения от клиента.
     * @throws IOException
     */
    private String readHeader() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        StringBuilder builder = new StringBuilder();
        String ln = null;
        while (true) {
            ln = reader.readLine();
            if (ln == null || ln.isEmpty()) {
                break;
            }
            builder.append(ln).append(System.getProperty("line.separator"));
        }
        return builder.toString();
    }

    /**
     * Вытаскивает идентификатор запрашиваемого ресурса из заголовка сообщения
     * от клиента.
     *
     * @param header заголовок сообщения от клиента.
     * @return идентификатор ресурса.
     */
    private String getURIFromHeader(String header) {
        int from = header.indexOf(" ") + 1;
        int to = header.indexOf(" ", from);
        String uri = header.substring(from, to);
        int paramIndex = uri.indexOf("?");
        if (paramIndex != -1) {
            uri = uri.substring(0, paramIndex);
        }
        return DEFAULT_FILES_DIR + uri;
    }

    /**
     * Отправляет ответ клиенту. В качестве ответа отправляется http заголовок и
     * содержимое указанного ресурса. Если ресурс не указан, отправляется
     * перечень доступных ресурсов.
     *
     * @param url идентификатор запрашиваемого ресурса.
     * @return код ответа. 200 - если ресурс был найден, 404 - если нет.
     * @throws IOException
     */
    private int send(String url) throws IOException {
        InputStream strm = HttpServer.class.getResourceAsStream(url);
        int code = (strm != null) ? 200 : 404;
        String header = getHeader(code);
        PrintStream answer = new PrintStream(out, true, "UTF-8");
        answer.print(header);
        if (code == 200) {
            int count = 0;
            byte[] buffer = new byte[1024];
            while ((count = strm.read(buffer)) != -1) {
                out.write(buffer, 0, count);
            }
            strm.close();
        }
        return code;
    }

    /**
     * Возвращает http заголовок ответа.
     *
     * @param code код результата отправки.
     * @return http заголовок ответа.
     */
    @SuppressWarnings("deprecation")
    private String getHeader(int code) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("HTTP/1.1 ").append(code).append(" ")
                .append(getAnswer(code)).append("\n");
        buffer.append("Date: ").append(new Date().toGMTString()).append("\n");
        buffer.append("Accept-Ranges: none\n");
        buffer.append("\n");
        return buffer.toString();
    }

    /**
     * Возвращает комментарий к коду результата отправки.
     *
     * @param code код результата отправки.
     * @return комментарий к коду результата отправки.
     */
    private String getAnswer(int code) {
        switch (code) {
            case 200:
                return "OK";
            case 404:
                return "Not Found";
            default:
                return "Internal Server Error";
        }
    }

    private Socket socket;
    private InputStream in = null;
    private OutputStream out = null;

    private static final String DEFAULT_FILES_DIR = "/www";
}

 Первое, что делает ClientSession - это получает содержимое запроса и выводит его в стандартный поток вывода. Т.к. данная реализация не предусматривает реакции на параметры запроса, то тело запроса не представляет интереса и чтение самого запроса ограничивается только его заголовком:
if (ln == null || ln.isEmpty()) {
    break;
}
Далее, из заголовка сообщения получается url запрашиваемого ресурса. В соответствии с протоколом http, url вытаскивается из первой строки заголовка как подстрока между первыми двумя пробелами. От url отрезаются параметры запроса (если они присутствуют).
private String getURIFromHeader(String header) {
    int from = header.indexOf(" ") + 1;
    int to = header.indexOf(" ", from);
    String uri = header.substring(from, to);
    int paramIndex = uri.indexOf("?");
    if (paramIndex != -1) {
        uri = uri.substring(0, paramIndex);
    }
    return DEFAULT_FILES_DIR + uri;
}

Для удобства, корневой директорией сервера считается папка www, но это всего лишь несущественная условность.

Для ответа клиенту формируется простейший http заголовок, указывающий код ответа. Доступны два кода: 200 - если запрашиваемый ресурс был найден и всеми любимый 404 - если ресурса не оказалось. Дополнительно в заголовке указывается время и тот факт, что сервер не поддерживает докачку файлов (оба поля не существенны и приводятся чисто формально).
private String getHeader(int code) {
    StringBuffer buffer = new StringBuffer();
    buffer.append("HTTP/1.1 ").append(code).append(" ")
    .append(getAnswer(code)).append("\n");
    buffer.append("Date: ").append(new Date().toGMTString()).append("\n");
    buffer.append("Accept-Ranges: none\n");
    buffer.append("\n");
    return buffer.toString();
}

При отправке ответа ресурсы получаются несколько неординарным способом:
private int send(String url) throws IOException {
    InputStream strm = HttpServer.class.getResourceAsStream(url);
    int code = (strm != null) ? 200 : 404;
    String header = getHeader(code);
    PrintStream answer = new PrintStream(out, true, "UTF-8");
    answer.print(header);
    if (code == 200) {
        int count = 0;
        byte[] buffer = new byte[1024];
        while ((count = strm.read(buffer)) != -1) {
            out.write(buffer, 0, count);
        }
        strm.close();
    }
    return code;
}
Это сделано для того, чтобы можно было уместить весь сервер в одном jar архиве.

Собранный проект вы можете взять здесь. Для запуска требуется java  версии 6 и выше. Команда для запуска проекта: java -jar webserver.jar <номер порта>. Номер порта можно не указывать, тогда сервер будет запущен на 9999 порту:
java -jar webserver.jar
Server started on port: 9999

Чтобы проверить работоспособность сервера, перейдите по адресу: http://localhost:9999/index.html В случае успеха должна открыться страница, которая лежит в папке www внутри jar архива с сервером. Для размещения собственных ресурсов на сервере просто добавьте их в папку www внутри архива и перейдите по ссылке http://localhost:9999/<путь>, где <путь> - это это путь к интересующему вас ресурсу относительно папки www внутри архива. Внимание! В <пути> в качестве разделителя необходимо использовать прямой слэш "/". Если путь будет содержать ссылку не на файл, а на директорию, будет выведено содержимое этой директории.

Содержимое, отправленное серверу можно увидеть в окне терминала из которого был запущен сервер. Помимо заголовка запроса будет выведен запрашиваемый адрес и код ответа сервера:
java -jar webserver.jar
Server started on port: 9999

GET /index.html HTTP/1.1
Host: localhost:9999
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:13.0) Gecko/20100101 Firefox/13.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive


Resource: /www/index.html

Result code: 200

Для прекращения работы сервера воспользуйтесь стандартной комбинацией клавиш Ctrl+C
UPD:  теперь доступен репозиторий, для  цикла статей об http в java:  https://github.com/programming086/java_http

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

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

HyperComments for Blogger

comments powered by HyperComments