openjdk.ruOpenJDK на русском

JEP 408: Simple Web Server

Простой веб-сервер

ОтветственныйJulia Boes
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск18
Компонентcore-libs / java.net
Обсуждениеnet dash dev at openjdk dot java dot net
ТрудоёмкостьS
ДлительностьS
РецензентыAlex Buckley, Brian Goetz, Chris Hegarty, Daniel Fuchs
ОдобренBrian Goetz
Создан2021/01/27 12:47
Обновлён2022/03/07 10:20
Задача8260510

Аннотация

Предоставить инструмент командной строки для запуска минимального веб-сервера, который отдаёт только статические файлы. Функциональность CGI или сервлетов не предусмотрена. Этот инструмент будет полезен для прототипирования, разового написания кода и тестирования, особенно в учебных целях.

Цели

  • Предложить готовый к использованию статический файловый HTTP-сервер с простой настройкой и минимальной функциональностью.

  • Снизить порог входа для разработчиков и сделать JDK доступнее.

  • Предоставить реализацию по умолчанию, доступную из командной строки, а также небольшой API для программного создания и настройки.

Что не является целью

  • Цель не в том, чтобы предоставить многофункциональный сервер или сервер коммерческого уровня. Гораздо лучшие альтернативы уже существуют: серверные фреймворки (например, Jetty, Netty и Grizzly) и промышленные серверы (например, Apache Tomcat, Apache httpd и NGINX). Эти полноценные технологии, оптимизированные по производительности, требуют усилий для настройки, а именно этого мы и хотим избежать.

  • Цель не в том, чтобы предоставить функции безопасности, такие как аутентификация, управление доступом или шифрование. Сервер предназначен исключительно для тестирования, разработки и отладки. Поэтому его устройство намеренно минимально, чтобы его нельзя было спутать с полнофункциональным серверным приложением.

Мотивация

Обычный обряд посвящения для разработчиков — выложить файл в веб, скорее всего HTML-файл «Hello, world!». Большинство учебных программ по информатике знакомят студентов с веб-разработкой, где обычно используются локальные тестовые серверы. Разработчики, как правило, изучают также системное администрирование и веб-сервисы — ещё одни области, где могут пригодиться инструменты разработки с базовой функциональностью сервера. Именно в таких учебных и неформальных задачах желателен небольшой готовый к использованию сервер. Сценарии использования:

  • Тестирование при веб-разработке, когда локальный тестовый сервер используется для имитации клиент-серверной конфигурации.

  • Тестирование веб-сервисов или приложений, когда статические файлы служат заглушками API в структуре каталогов, которая повторяет RESTful URL и содержит фиктивные данные.

  • Неформальный просмотр файлов и обмен ими между системами, например чтобы искать в каталоге на удалённом сервере со своей локальной машины.

Во всех этих случаях мы, конечно, можем использовать фреймворк веб-сервера, но у этого подхода высокий порог входа: прежде чем обслужить первый запрос, нужно поискать варианты, выбрать один, скачать его, настроить и разобраться, как им пользоваться. Все эти шаги складываются в изрядное количество церемоний, и это недостаток; если где-то на этом пути застрять, это может расстроить и даже помешать дальнейшему использованию Java. Простой веб-сервер, запущенный из командной строки или несколькими строками кода, позволяет обойтись без этих церемоний и сосредоточиться на текущей задаче.

Python, Ruby, PHP, Erlang и многие другие платформы предлагают готовые серверы, запускаемые из командной строки. Такое разнообразие существующих альтернатив показывает, что потребность в инструменте такого рода признана.

Описание

Simple Web Server — минимальный HTTP-сервер для раздачи одной иерархии каталогов. Он основан на реализации веб-сервера в пакете com.sun.net.httpserver, который входит в JDK с 2006 года. Пакет официально поддерживается, и мы расширяем его API, которые упрощают создание сервера и улучшают обработку запросов. Simple Web Server можно использовать через специальный инструмент командной строки jwebserver или программно через его API.

Инструмент командной строки

Следующая команда запускает Simple Web Server:

$ jwebserver

Если запуск прошёл успешно, jwebserver выводит в System.out сообщение с локальным адресом и абсолютным путём к раздаваемому каталогу. Например:

$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /cwd and subdirectories on 127.0.0.1 port 8000
URL: http://127.0.0.1:8000/

По умолчанию сервер работает на переднем плане и привязывается к loopback-адресу и порту 8000. Это можно изменить с помощью параметров -b и -p. Например, чтобы запустить сервер на порту 9000, выполните:

$ jwebserver -p 9000

Например, чтобы привязать сервер ко всем интерфейсам:

$ jwebserver -b 0.0.0.0
Serving /cwd and subdirectories on 0.0.0.0 (all interfaces) port 8000
URL: http://123.456.7.891:8000/

По умолчанию файлы раздаются из текущего каталога. Другой каталог можно указать параметром -d.

Обслуживаются только идемпотентные запросы HEAD и GET. На любые другие запросы возвращается ответ 501 - Not Implemented или 405 - Not Allowed. Запросы GET отображаются на раздаваемый каталог следующим образом:

  • Если запрошенный ресурс — файл, отдаётся его содержимое.
  • Если запрошенный ресурс — каталог, содержащий индексный файл, отдаётся содержимое индексного файла.
  • В остальных случаях выводится список имён всех файлов и подкаталогов этого каталога. Символические ссылки и скрытые файлы не выводятся в списке и не отдаются.

Simple Web Server поддерживает только HTTP/1.1. Поддержки HTTPS нет.

MIME-типы настраиваются автоматически. Например, файлы .html отдаются как text/html, а файлы .java — как text/plain.

По умолчанию каждый запрос записывается в журнал на консоли. Вывод выглядит так:

127.0.0.1 - - [10/Feb/2021:14:34:11 +0000] "GET /some/subdirectory/ HTTP/1.1" 200 -

Вывод журнала можно изменить параметром -o. Значение по умолчанию — info. При значении verbose дополнительно выводятся заголовки запроса и ответа, а также абсолютный путь к запрошенному ресурсу.

После успешного запуска сервер работает, пока его не остановят. На платформах Unix сервер можно остановить, отправив ему сигнал SIGINT (Ctrl+C в окне терминала).

Параметр -h выводит справку со списком всех параметров, которые следуют рекомендациям из JEP 293. Также доступна man-страница jwebserver.

Options:
       -h or -? or --help
              Prints the help message and exits.

       -b addr or --bind-address addr
              Specifies the address to bind to.  Default: 127.0.0.1 or ::1 (loopback).  For
              all interfaces use -b 0.0.0.0 or -b ::.

       -d dir or --directory dir
              Specifies the directory to serve.  Default: current directory.

       -o level or --output level
              Specifies the output format.  none | info | verbose.  Default: info.

       -p port or --port port
              Specifies the port to listen on.  Default: 8000.

       -version or --version
              Prints the version information and exits.

       To stop the server, press Ctrl + C.

API

Инструмент командной строки полезен, но что, если нужно использовать компоненты Simple Web Server (то есть сервер, обработчик и фильтр) с существующим кодом или дополнительно настроить поведение обработчика? Часть настроек доступна в командной строке, но лаконичное и интуитивно понятное программное решение для создания и настройки повысило бы полезность компонентов сервера. Чтобы преодолеть разрыв между простотой инструмента командной строки и подходом «напиши сам» текущего API com.sun.net.httpserver, мы определяем новые API для создания сервера и настраиваемой обработки запросов.

Новые классы — SimpleFileServer, HttpHandlers и Request; каждый из них построен на существующих классах и интерфейсах пакета com.sun.net.httpserver: HttpServer, HttpHandler, Filter и HttpExchange.

Класс SimpleFileServer поддерживает создание файлового сервера, обработчика файлового сервера и фильтра вывода:

package com.sun.net.httpserver;

public final class SimpleFileServer {
    public static HttpServer createFileServer(InetSocketAddress addr,
                                              Path rootDirectory,
                                              OutputLevel outputLevel) {...}
    public static HttpHandler createFileHandler(Path rootDirectory) {...}
    public static Filter createOutputFilter(OutputStream out,
                                            OutputLevel outputLevel) {...}
    ...
}

С помощью этого класса минимальный, но настроенный сервер можно запустить несколькими строками кода в jshell:

jshell> var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
   ...> Path.of("/some/path"), OutputLevel.VERBOSE);
jshell> server.start()

Настроенный обработчик файлового сервера можно добавить к существующему серверу:

jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler());
jshell> var handler = SimpleFileServer.createFileHandler(Path.of("/some/path"));
jshell> server.createContext("/browse/", handler);
jshell> server.start();

Настроенный фильтр вывода можно добавить к серверу при его создании:

jshell> var filter = SimpleFileServer.createOutputFilter(System.out,
   ...> OutputLevel.INFO);
jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler(), filter);
jshell> server.start();

Два последних примера возможны благодаря новым перегруженным методам create в классах HttpServer и HttpsServer:

public static HttpServer create(InetSocketAddress addr,
                                int backlog,
                                String root,
                                HttpHandler handler,
                                Filter... filters) throws IOException {...}

Улучшенная обработка запросов

Основную функциональность Simple Web Server обеспечивает его обработчик. Чтобы этот обработчик можно было расширять для использования с существующим кодом, мы вводим новый класс HttpHandlers с двумя статическими методами для создания и настройки обработчиков, а также новый метод в классе Filter для адаптации запроса:

package com.sun.net.httpserver;

public final class HttpHandlers {
    public static HttpHandler handleOrElse(Predicate<Request> handlerTest,
                                           HttpHandler handler,
                                           HttpHandler fallbackHandler) {...}
    public static HttpHandler of(int statusCode, Headers headers, String body) {...}
    {...}
}

public abstract class Filter {
    public static Filter adaptRequest(String description,
                                      UnaryOperator<Request> requestOperator) {...}
    {...}
}

handleOrElse дополняет условный обработчик другим обработчиком, а фабричный метод of позволяет создавать обработчики с заранее заданным состоянием ответа. Фильтр предварительной обработки, полученный из adaptRequest, можно использовать, чтобы проверить и изменить определённые свойства запроса до его обработки. Сценарии использования этих методов: делегирование обменов в зависимости от метода запроса, создание обработчика с «заготовленным ответом», который всегда возвращает определённый ответ, или добавление заголовка ко всем входящим запросам.

Существующий API представляет HTTP-запрос как часть пары «запрос — ответ», представленной экземпляром класса HttpExchange, который описывает полное и изменяемое состояние обмена. Не всё это состояние имеет значение для настройки и адаптации обработчиков. Поэтому мы вводим более простой интерфейс Request, который даёт ограниченное представление неизменяемого состояния запроса:

public interface Request {
    URI getRequestURI();
    String getRequestMethod();
    Headers getRequestHeaders();
    default Request with(String headerName, List<String> headerValues)
    {...}
}

Это позволяет легко настраивать существующий обработчик, например:

jshell> var h = HttpHandlers.handleOrElse(r -> r.getRequestMethod().equals("PUT"),
   ...> new SomePutHandler(), new SomeHandler());
jshell> var f = Filter.adaptRequest("Add Foo header", r -> r.with("Foo", List.of("Bar")));
jshell> var s = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/", h, f);
jshell> s.start();

Альтернативы

Мы рассматривали альтернативу для инструмента командной строки:

  • java -m jdk.httpserver: изначально Simple Web Server запускался командой java -m jdk.httpserver, а не специальным инструментом командной строки. Это по-прежнему возможно (на самом деле jwebserver внутри использует команду java -m ...), но мы решили ввести специальный инструмент ради удобства и доступности.

При прототипировании мы рассматривали несколько альтернатив API:

  • Новый класс DelegatingHandler — собрать методы настройки в отдельном классе, реализующем интерфейс HttpHandler. Мы отказались от этого варианта, так как он вводит новый тип, не добавляя функциональности. Кроме того, этот новый тип было бы трудно обнаружить. Класс HttpHandlers, напротив, использует шаблон outboarding (вынесение), при котором статические вспомогательные методы или фабрики класса собираются в новом классе. Почти идентичное имя позволяет легко найти класс, облегчает понимание и использование новых точек API и скрывает детали реализации делегирования.

  • HttpHandler как сервис — сделать HttpHandler сервисом и предоставить внутреннюю реализацию обработчика файлового сервера. Разработчик мог бы либо предоставить собственный обработчик, либо использовать провайдер по умолчанию. Недостаток этого подхода в том, что им сложнее пользоваться и он довольно громоздок для того небольшого набора функциональности, который мы хотим предоставить.

  • Filter вместо HttpHandler — использовать для обработки запроса только фильтры, а не обработчики. Фильтры обычно выполняют предварительную или последующую обработку, то есть обращаются к запросу до или после вызова обработчика, например для аутентификации или журналирования. Однако они не были задуманы как полная замена обработчиков. Использовать их так было бы неинтуитивно, а методы было бы труднее найти.

Тестирование

Основную функциональность инструмента командной строки обеспечивает API, поэтому большая часть усилий по тестированию будет сосредоточена на API. Точки API можно тестировать изолированно модульными тестами и существующим тестовым фреймворком. Особое внимание мы уделим доступу к файловой системе и очистке URI. Тесты API мы дополним тестированием команд и базовыми проверками работоспособности инструмента командной строки.

Риски и допущения

Этот простой сервер предназначен только для тестирования, разработки и отладки. В этих рамках на него распространяются общие проблемы безопасности серверов, и они будут решаться следованием лучшим практикам безопасности и тщательным тестированием.