Как написать простой http сервер

Время на прочтение
4 мин

Количество просмотров 30K

Думаю что не будет преувеличением утверждать, что знание и понимание сути протокола HTTP необходимо любому, кто решил сколь-нибудь серьезно заняться любым из направлений современной Web разработки. Мой личный опыт говорит о том, что понимание это приходит не сразу. Стыдно сказать, что были времена, когда слова GET и POST были для меня сродни магическим заклинаниям, а о существовании PUT, PATCH и DELETE я даже не подозревал.

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

Как оказалось сделать это совсем не сложно. Ниже привожу код, которого будет достаточно для корректного взаимодействия с любым браузером! Все что нам понадобится это ServerSocket и немного стандартного ввода-вывода.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class HttpServer {

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("Server started!");
            
            while (true) {
                // ожидаем подключения
                Socket socket = serverSocket.accept();
                System.out.println("Client connected!");

                // для подключившегося клиента открываем потоки 
                // чтения и записи
                try (BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
                     PrintWriter output = new PrintWriter(socket.getOutputStream())) {

                    // ждем первой строки запроса
                    while (!input.ready()) ;

                    // считываем и печатаем все что было отправлено клиентом
                    System.out.println();
                    while (input.ready()) {
                        System.out.println(input.readLine());
                    }

                    // отправляем ответ
                    output.println("HTTP/1.1 200 OK");
                    output.println("Content-Type: text/html; charset=utf-8");
                    output.println();
                    output.println("<p>Привет всем!</p>");
                    output.flush();
                    
                    // по окончанию выполнения блока try-with-resources потоки, 
                    // а вместе с ними и соединение будут закрыты
                    System.out.println("Client disconnected!");
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

Пробуем запустить этот код. Стоит отметить, что порт, для которого создается ServerSocket должен быть свободным. Если указанный порт занят, то нужно или его освободить, или использовать другой свободный порт.

После запуска этого кода идем в окно браузера и набираем в адресной строке http://localhost:8080/. Если все прошло удачно, то в окне браузера мы увидим текст «Привет всем», а в логе сервера текст, подобный приведенному ниже:

Server started!
Client connected!

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7,he;q=0.6,de;q=0.5,cs;q=0.4
Cookie: _ga=GA1.1.1849608036.1549463927; portainer.pagination_containers=100; _gid=GA1.1.80775985.1550669456;
If-Modified-Since: Sat, 05 Jan 2019 12:10:16 GMT

Client disconnected!

Каждый раз, когда мы что-то вводим в адресную строку браузера и нажимаем Enter не происходит ничего иного, как отправка текста, начинающегося словом GET и заканчивающегося переводом строки. После слова GET через пробел следует путь к запрашиваемому документу на сервере. Попробуйте ввести в браузере http://localhost:8080/something и посмотреть, как изменится текст запроса в логе.

В строках запроса, начиная со второй находятся т.н. заголовки при помощи которых осуществляется передача серверу информации о настройках клиента. Каждая строка заголовка имеет формат [имя заголовка] : [значение]; [значение]; ... [значение].

После того, как текст запроса полностью прочитан сервером, мы отправляем ему простейший ответ, структура которого довольно проста и аналогична структуре запроса. В первой строке версия протокола HTTP и код 200 OK, который сообщит браузеру о том, что запрос был успешно обработан (всем куда лучше знаком код 404, не правда ли ;) ). Далее следует всего один заголовок Content-Type в котором передается информация о формате передаваемого документа (text/html) и его кодировке (charset=utf-8). После заголовка следует перевод строки (обязательное требование протокола HTTP) и собственно текст, который будет отображен в браузере.

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

UPD. Гораздо более продвинутый пример подобного сервера можно найти в книге How Tomcat Works: A Guide to Developing Your Own Java Servlet Container by Paul Deck, Budi Kurniawan, глава 1 — Simple Web Server.

UPD1. Вебинар на основе этой статьи www.youtube.com/watch?v=RBUFdFKg_rI

Программирование на Java – перспективное направление у современных разработчиков. Согласно данным Google данный язык кодинга пользуется огромным спросом, несмотря на то, что был создан изначально весьма давно. А именно – в 1995 году. С тех пор Джава перетерпел много модификаций. Нынче это – один из самых перспективных языков программирования. Весьма прост в освоении, что значительно упрощает задачи, поставленные перед новичками.

Особенности Java

Google и другие поисковые системы помогут понять, что это за язык программирования. Он имеет ряд ключевых особенностей. А именно:

  • относительно простой синтаксис;
  • отличное комьюнити;
  • множество документации (в том числе на русском языке);
  • наличие ООП;
  • собственный движок.

New programs создавать при помощи данного варианта способен даже начинающий программист.

Основное предназначение Java – это работа с Сетью. Идеально подходит для веб-программирования. Но и «обычные» утилиты посредством соответствующего семейства пишутся без существенных затруднений.

Сервер типа http – определение

При работе с сетью (new или old – не так важно) компьютеры подключаются к так называемым веб-серверам. Без них невозможна работа в интернете.

HTTP-сервер – это веб server. Имеет непосредственное отношение как к «железу» компьютера, так и к программному обеспечению:

  1. В качестве аппаратного устройства это – new машина, которая отвечает за хранение ресурсов того или иного сайта. Включает в себя доставку на устройство юзера через интернет-обозреватели и иные утилиты. Чаще всего подключается к интернету. Доступ предоставляется через доменные имена.
  2. Как ПО, согласно Google, веб-сервер представляет собой некое «приложение», совмещающее в себе функционал для контроля доступа web-пользователей к размещенным на сервере документов. HTTP Sever – часть программного обеспечения, понимающая URLs и HTTP-протоколы (они нужны для просмотра и отображения страничек в Сети).

Google указывает на то, что, когда браузер нуждается в каком-то файле, помещенном на веб-сервере, происходит запрос посредством http. Когда запрос доходит до нужного «железа», соответствующий сервер (программное обеспечение) осуществляет обратную передачу через упомянутый ранее «канал».

Виды серверов

Если разработчик на Джаве решил создать new servers, важно понимать – для опубликования сайта требуется подобрать один из нескольких видов оных. Упомянутый элемент бывает:

  1. Статическим (static void main). Носит название стека. Включает в себя компьютер с сервером HTTP. Последний будет посылать new файлы в интернет-обозреватель без каких-либо корректировок.
  2. Динамическим. Включает в себя статическую «модель» и дополнительное ПО. Чаще всего – базы данные или серверные приложения. Последние будут вносить изменения в документы перед тем, как отправить их в обозреватель Сети.

За счет new servers можно отображать страницы в браузерах. Итоговый результат удобен и понятен пользователям без навыков в сфере программирования. Для своей работы, согласно Google, рассматриваемый элемент может задействовать шаблоны информации из БД Пример – Википедия. Это – не полноценный сайт, а HTML-шаблон. За счет соответствующего приема удается значительно ускорить сопровождение web-софта.

Все наготове – встроенные возможности Java

При помощи Google и иных поисковиков можно достаточно быстро разобраться в упомянутом элементе, задействованном при программировании. Но не каждый пользователь готов заниматься созданием полноценного сервисного контента.

Для Джава поставленная задача не выступает в качестве тривиальной. Язык программирования содержит встроенные возможности, посредством которых new http создается без существенных затруднений. Всего 100 строчек кода – и перед разработчиком окажется весьма неплохой вариант, поддерживающий обработку запросов и иные HTTP-команд.

HTTPServer

Серверные возможности в Джаве предоставляются через SDK. Они имеют следующие особенности:

  • название – HttpServer;
  • пакетный класс – com.sun.net;
  • запись: httpServer server = httpServer.create (new InetSocketAddress(“localhost”, 8001), 0));.

Приведенный пример – это создание экземпляра в пределах локального узла. Номер порта – 8001. Аргумент 0 здесь выступает для организации так называемой обратной регистрации.

Как выполняется запрос – очередность

Если рассматриваемый элемент работает исправно, он будет отправлять и получать разнообразные команды-запросы. В Google по этому поводу очень много информации. Новички в ней рискуют запутаться.

Чтобы понимать принципы работы серверных возможностей Джавы, стоит уяснить, какой очередности подчиняется обработка посланного запроса:

  1. Клиент формирует запрос. Происходит его отправка.
  2. Ставится очередь операционной системы.
  3. Происходит передача на сервер для дальнейшей обработки.
  4. Одновременные запросы ставятся в очередь. Их количество определяется ОС автоматически.

Важно: предложенный пример не требует отправлять в очередь никаких «команд», так как имеет значение аргумента, равное нулю.

О коде

Вот пример, который поможет лучше разобрать в изучаемой сфере даже без Google:

Здесь происходит создание контекста test. Он выступает корнем контекста утилиты. Второй параметр – экземпляр так называемого обработчика. Он будет работать с HTTP-командами.

Теперь допускается применение потокового пула. В приведенном примере их 10 штук:

New Thread PoolExecutor ThreadPoolExecutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);

Далее new server требует запуска. Операция осуществляется путем задействования кода: server.start();.

Handler

А вот интерфейс согласно данным Google, использующий метод handle():

Внимание: в приведенном примере вышедший за рамки изображения код имеет вид   private void (может быть и public static void) handleResponse(HttpExchange httpExchange, String requestParamValue)  throws  IOException {

        OutputStream outputStream = httpExchange.getResponseBody();

        StringBuilder htmlBuilder = new StringBuilder();

Кодификация обрабатывает запрос, затем отправляет ответ непосредственно клиенту. Обработка осуществляется через класс HttpExchange.

Запрос GET

Об обработке запроса Get необходимо знать следующее:

  • написавшие его будут использовать метод HandleGETRequest;
  • далее происходит вызов getRequestURL(), который принадлежит классу HttpExchange.

Несмотря на то, что это – минимум, обрабатывающий единичный запрос, он поможет справиться с самыми разными задачами.

Работа с ответом

После того, как произошел поиск ответа, его нужно направить клиенту. Делается это через handleResponse(). Пользователь получит выходной поток через обращение к методу gerResponseBody(). Чуть позже удастся записать информацию из HTML в выходные потоки.

Response header – это крайне важный момент. Если он будет упущен, в обозревателе Сети юзер увидит ошибку ERR_Empty_Response. В случае, когда все хорошо, браузер покажет тот или иной ответ.

Близкое знакомство – собственный чат через WebSoket и SpringBoot на Java

Сегодня трудно представить юзера, который не умел бы работать в интернете. Здесь не только отдыхают, но и отправляют сообщения, трудятся, создают что-то полезное. Коммуникации помогает поддерживать так называемый чат. И это – элементарный серверный пример.

В Google можно отыскать немало new идей относительно того, как создать собственный chat. При определенной сноровке соответствующий ресурс будет действительно уникальным: со смайликами, эмодзи, анимацией и другими элементами.

Для написания подобного контента в Java можно использовать:

  • WebSoket;
  • SpringBoot.

При помощи соответствующих элементов даже новичок сможет без труда разобраться с поставленной задачей. Ему не придется долго изучать Google, а также «непонятные» элементы кода (типа ioexception e, public void run, override public и так далее – на первых порах в них можно запутаться).

Определение WebSoket

WebSoket – это протокол, при помощи которого осуществляется установка двусторонней связи клиент-сервер. Переключение, как говорит Google, происходит после специального http-запроса. Его формирует и отправляет клиент: Upgrade: websocket.

При поддержке вебсокетов будет получен ответ «Yes». Далее произойдет общение через new протоколы WebSocket. С HTTP оный не имеет ничего общего.

Как создать приложение

Для того, чтобы сделать new application, можно прибегнуть к работе с сайтом-инициализатором. В этом случае алгоритм действий будет следующим:

  1. Зайти на страницу.
  2. Выбрать в списке Spring Boot 2.
  3. Указать имя группы и артефакта проекта.
  4. Активировать зависимость «вебсокета».
  5. Провести генерацию new project.

Далее предстоит разархивировать проект, а также сделать import java в редактор. Именем утилиты будет – Maven. Дополнительно необходимо создать пакеты config, controller и model.

Настройка

Теперь предстоит работать с public class, а также с такими элементами как static final int и private static. Ведь самое простое позади – далее требуется провести настройку «вебсокета».

Начинается процесс с конечной точки и брокера сообщений. Проводится операция в config. Класс конфигурации будет иметь следующий вид:

Здесь:

  • аннотация @configuration – устанавливается обязательно в классе конфигурации Spring;
  • аннотация @EnableWebSocketMessageBroker – активирует new WebsokcketServer;
  • метод registerStompEndpoints() – отвечает за регистрацию конечной точки, которую клиенты задействуют для подключения к серверу;
  • configMessageBroker() – настройка брокера для отправки сообщений между клиентами.

В рассматриваемом примере задействован встроенный брокер. Это самый простой вариант.

Модели сообщений

Следующие исходники – это настройка моделей сообщений. Создается пакет model, в котором после размещается класс ChatMessage:

Соответствующий фрагмент кода еще не позволит отправлять текст друг другу. Для реализации поставленной задачи предстоит выполнить иные действия.

Контроллер сообщений

Google поможет разобраться в том, как создавать собственные чаты. Чтобы в них можно было отправлять сообщения и получать оные, стоит:

  • сделать пакет controller;
  • разместить внутри класс chatController;
  • внутри ЧатКонтроллер присутствуют методы, которые отвечают за доставку сообщений от одного пользователя и трансляцию всем остальным.

Для настройки контроллера используется следующая кодификация:

События и Front-End

Немаловажно настроить события подключение/отключение. Это необходимо для передачи сообщений на всеобщее обозрение:

Статистика – тоже немаловажный нюанс. If юзер хочет получить полноценный чат, ему предстоит выйти за пределы Джавы.

Чтобы справиться с поставленной задачей требуется:

  • сделать папку static;
  • расположить ее по пути scr/main/resources.

Выглядеть это будет так:

HTML и скрипты

В Google также говорится о том, что для работы полноценного чата требуется создать HTML-файл и JavaScript.

В первом случае используется new запись:


Скрипт Джавы требуется для соединения с итоговой точкой, а также отправки/получения сообщений. Он будет иметь имя main.js:



Стилизация

Исходники CSS можно создавать самостоятельно или подключать уже готовые варианты. Данная «опция» отвечает за внешний вид программы.

Здесь можно найти исходники соответствующего кода (пункт 7).

Запуск

Все, что теперь остается – это проверить Spring через Boot-файл с функцией main в корне иерархии имеющихся папок.

После запуска веб-сервера требуется перейти по адресу и пользоваться созданным контентом.

Для того, чтобы лучше разбираться в соответствующей сфере и термины int port, close и иные составляющие Джавы не были чем-то непонятным, стоит закончить специализированные курсы. Они без Google помогут освоить Java, а также всего его тонкости.

Архитектура Web-приложений

Архитектура современных web-приложений

Архитектура современных web-приложений

Архитектура современных приложений состоит из отдельных модулей, как показано на рисунке выше. Эти модули часто называют Frontend и Backend. Frontend – это модуль, который отвечает за юзер-интерфейс и логику, которые предоставляется приложением при использовании. Так, например когда мы заходим в соцсети через браузер, мы взаимодействуем именно с FrontEnd-модулем приложения. То, как отображаются наши посты в виде сторисов или карточек, сообщения и другие активности реализуются именно в FrontEnd-модуле. А все данные, которые мы видим, хранятся и обрабатываются в Backend или серверной части приложения. Эти модули обмениваются между собой посредством разных архитектурных стилей: REST, GRPC и форматов сообщений – JSON и XML.

В этой статье мы напишем примитивную серверную часть социальной сети с использованием Spring Boot, запустим свой сервер, рассмотрим разные типы HTTP запросов и их применение.

Необходимое требование к читателю: умение писать на Java и базовые знания Spring Framework. Данная статья познакомит вас со Spring Boot и даст базовые понятия данного фреймворка.

Инициализация проекта

Чтобы создать Spring Boot проект, перейдем на страницу https://start.spring.io/ и выберем необходимые зависимости: в нашем случае Spring Web. Чтобы запустить проект, необходима минимальная версия Java 17. Скачиваем проект и открываем в любом IDE (в моем случае – Intellij Idea)

Инициализация проекта

Инициализация проекта

Spring Web – зависимость, которая предоставляет контейнер сервлетов Apache Tomcat (является дефолтным веб-сервером). Проще говоря, сервлеты – это классы, которые обрабатывают все входящие запросы.

Открываем проект и запускаем.

Запуск проекта

Запуск проекта

Мы видим, что проект запустился и готов обрабатывать запросы на порту 8080 – Tomcat started on port(s): 8080 (http).

Теперь создадим свой первый класс – GreetingController. Controller-классы ответственны за обработку входящих запросов и возвращают ответ.
Чтобы сделать наш класс Controller, достаточно прописать аннотацию @RestController. @RequestMapping указывает, по какому пути будет находиться определённый ресурс или выполняться логика.

Greeting Controller
        package io.proglib;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/greet")
public class GreetingController {

    @GetMapping
    public String greet() {
        return "Hello";
    }

}

    

Перезапускаем проект, и сервер готов уже обрабатывать наши запросы.
Открываем браузер по адресу http://localhost:8080/greet и получаем следующий вывод.

Ответ сервера

Ответ сервера

Если отправить запрос по адресу http://localhost:8080/ , мы получим ошибку, т. к. по этому пути не определены логика обработки запроса и ресурсы.

Request Params

При отправке запросов мы часто используем переменные в запросе, чтобы передавать дополнительную информацию или же делать запросы гибкими. Параметр в запросе передаётся в конце адреса (=url) сервера и указывается после вопросительного знака (=?).
Например, http://localhost:8080/greet?name=Alice. Параметр запроса является = name cо значением = Alice.

Чтобы обрабатывать переменную запроса, используется аннотация @RequestParam. Параметры запроса могут быть опциональными или же обязательными. @RequestParam("name") означает следующее: взять ту переменную из запроса, название которого равно name.

Метод с параметризированным запросом
        @RestController
@RequestMapping("/greet")
public class GreetingController {

    @GetMapping
    public String greet(@RequestParam("name") String name) {
        return "Hello, " + name;
    }

}

    

☕ Пишем свой первый сервер на Java и Spring Boot

Вдобавок, запрос может содержать несколько параметров.

Например, http://localhost:8080/greet/full?name=John&surname=Smith. Параметры выделяются знаком &. В этом запросе два параметра: name=John и surname=Smith.

Чтобы обработать каждый параметр запроса, нужно пометить каждую переменную @RequestParam.

        package io.proglib;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/greet")
public class GreetingController {

    @GetMapping
    public String greet(@RequestParam("name") String name) {
        return "Hello, " + name;
    }

    @GetMapping("/full")
    public String fullGreeting(@RequestParam("name") String name, 
                               @RequestParam("surname") String surname) {
        return "Nice to meet you, " + name + " " + surname;
    }

}

    

Параметризованный запрос с двумя параметрами

Параметризованный запрос с двумя параметрами

Path Variable

PathVariable по применению похож на @Request Param. @PathVariable также является параметром запроса, но используются внутри адреса запроса. Например,

RequestParamhttp://localhost:8080/greet/full?name=John&surname=Smith
PathVariablehttp://localhost:8080/greet/John. В этом случае John является PathVariable.
В запросе можно указывать несколько PathVariable, как и в случае RequestParam

        package io.proglib;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/greet")
public class GreetingController {

    @GetMapping
    public String greet(@RequestParam("name") String name) {
        return "Hello, " + name;
    }

    @GetMapping("/full")
    public String fullGreeting(@RequestParam("name") String name, @RequestParam("surname") String surname) {
        return "Nice to meet you, " + name + " " + surname;
    }

    @GetMapping("/{name}")
    public String greetWithPathVariable(@PathVariable("name") String name) {
        return "Hello, " + name;
    }

}

    

Чтобы протестировать, открываем браузер и переходим по адресам: http://localhost:8080/greet/John/Smith и http://localhost:8080/greet/John

☕ Пишем свой первый сервер на Java и Spring Boot

Запрос с двумя параметризованными PathVariable.

☕ Пишем свой первый сервер на Java и Spring Boot

HTTP-методы

Когда мы говорим о запросах, мы также подразумеваем HTTP-метод, который используется при отправке этого запроса. Каждый запрос представляет собой некий HTTP-метод. Например, когда мы переходим в браузере по адресу http://localhost:8080/greet/John/Smith, наш браузер отправляет GET-запрос на сервер.

Большая часть информационных систем обмениваются данными посредством HTTP-методов. Основными HTTP-методами являются – POST, GET, PUT, DELETE. Эти четыре запроса также называют CRUD-запросами.

  • POST-метод – используется при создании новых ресурсов или данных. Например, когда мы загружаем новые посты в соцсетях, чаще всего используется POST-запросы. POST-запрос может иметь тело запроса.
  • GET-метод – используется при получении данных. Например, при открытии любого веб-приложения, отправляется именно GET-запрос для получения данных и отображения их на странице. GET-запрос не имеет тела запроса.
  • PUT-метод – используется для обновления данных, а также может иметь тело запроса, как и POST.
  • DELETE-метод – используется для удаления данных.

Реализация основных методов

Давайте создадим сущности и реализуем методы, чтобы наш сервер принимал все четыре запроса. Для этого создадим сущности User и Post, и будем проводить операции над ними.

Для простоты User имеет только два поля: username и список постов posts, а сущность Post имеет поле description и imageUrl.

Сущность User:

        package io.proglib;

import java.util.ArrayList;
import java.util.List;

public class User {
    private String username;
    private List<Post> posts;

    public User() {
        posts = new ArrayList<>();
    }

    public User(String username, List<Post> posts) {
        this.username = username;
        this.posts = posts == null ? new ArrayList<>() : posts;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }

}

    

Сущность Post:

        package io.proglib;

public record Post(
        String description,
        String imageUrl
) {
}

    

Создаем новый класс контроллер – UserActivityController, который будет обрабатывать наши запросы – POST, GET, PUT, DELETE.

Наш контроллер: UserActivityController.

Будем использовать список – List<User> users в качестве локальной базы данных, где будем хранить все наши данные.

UserActivityController
        package io.proglib;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserActivityController {

      private final static List<User> users = new ArrayList<>();

}

    

POST-запрос: добавление нового пользователя

Чтобы указать, что метод принимает POST-запросы используем аннотацию – @PostMapping. Так как запрос имеет тело запроса, где мы передаем пользователя, нужно пометить переменную user аннотацией @RequestBody.

            @PostMapping("")
    public User addUser(@RequestBody User user) {
        users.add(user);
        return user;
    }
    

GET-запрос: получение пользователей

         @GetMapping("")
    public List<User> getUsers() {
        return users;
    }

    @GetMapping("/{username}")
    public User getUserByUsername(@PathVariable("username") String username) {
        return users.stream().filter(user -> user.getUsername().equals(username))
                .findFirst().get();
    }


    

@Getmapping("") указывает, что методы обрабатывают GET-запросы. Значение, которое передаётся внутри аннотации, является частью url или адреса. Например, запрос http://localhost:8080/users/baggio обработается методом getUserByUsername(), а запрос http://localhost:8080/users/ обработается методом http://localhost:8080/users.

PUT-запрос: обновление данных

            @PutMapping("/{username}")
    public Post update(@PathVariable("username") String username, @RequestBody Post post) {
        users.stream().filter(user ->
                        user.getUsername().equals(username))
                .findAny()
                .ifPresent(user -> user.getPosts().add(post));
        return post;
    }
    

@PutMapping("/{username}") указывает, что метод принимает PUT-запросы. В нашем примере в запросе мы передаем параметр запроса, а также тело запроса – post. Метод принимает – username, ищет юзера из списка с таким username и добавляем новый пост к его списку постов.

Delete-запрос: удаление данных

        @DeleteMapping("/{username}")
    public String deleteUser(@PathVariable("username") String username) {
        users.stream().filter(user ->
                        user.getUsername().equals(username))
                        .findAny()
                                .ifPresent(users::remove);
        return "User with username: " + username + " has been deleted";
    }
    

@DeleteMapping("/{username}") – указывает, что метод принимает DELETE-запросы.
Данный метод получает параметр username из запроса и удаляет из списка пользователя с таким username.

Запуск приложения и тестирование

Чтобы убедиться, что все работает, мы можем отправить каждый вид запроса и протестировать. Для этого нам необходим API-client, который может посылать запросы. В примерах я использую POSTMAN.

Post-запрос: создание нового пользователя.

Тело запроса отправляется в виде JSON.

☕ Пишем свой первый сервер на Java и Spring Boot

GET-запрос: получение пользователей

☕ Пишем свой первый сервер на Java и Spring Boot

PUT-запрос: обновление списка постов пользователя

☕ Пишем свой первый сервер на Java и Spring Boot

DELETE-запрос: удаление пользователя по username

☕ Пишем свой первый сервер на Java и Spring Boot

***

В этой статье мы рассмотрели архитектуру современных web-приложений, а также написали свою серверную часть приложения, получив поверхностные знания по Spring Boot, HTTP запросы и параметры запросов.

Ссылка на репозиторий

Исходный код можно найти по ссылке.

Материалы по теме

  • ☕ Сертификаты и тренинги для Java-разработчика
  • ☕🛣️ Дорожная карта Java-разработчика в 2023 году: путь с нуля до первой работы

Introduction

Java 18 made its general availability on March 22nd 2022 (a non-LTS version). One of the features it had was to start a simple web server via the command line to serve static files. In this post, we will learn about the Java 18 simple web server (command-line tool) released as part of JDK 18.

Java 18 Simple Web Server command-line tool

The goal of JEP 408 was to provide a simple command-line tool to launch a minimal web server to serve static files. The motive was to use it for prototyping and to launch a simple web server to serve files quickly. In other words, running the command-line tool will create and start an out-of-the-box HTTP file server. The file server will use some default configuration with command-line options to tweak some.

It was not meant as an alternate or to compete with full-blown production web servers like Apache Tomcat, NGINX etc., Also, it doesn’t have any authentication mechanism, access control or encryption. It was meant to be used for testing, development and debugging.

With this, we don’t have to write Java code to have a web server running or setup a full-blown web server framework.

The simple web server offered via command-line tool is a minimal HTTP server which serves a single directory hierarchy. It is based on the web server implementation in com.sun.net.httpserver package. The simple web server will only support HTTP/1.1 and there is no HTTPS support.

Directory organization and structure

For this post, I’ll use a directory named JavaDevCentral present inside the /tmp folder which has the below organization and content.

/tmp
├── JavaDevCentral
│   ├── GoogleGuava
│   │   └── guava-cache.txt
│   ├── Java11
│   │   └── optional-notes.txt
│   ├── Java8
│   │   └── stream-notes.txt
│   └── java-notes.txt

Starting Java Simple Web Server from the command line

Once we have Java 18 installed, we can start the Simple Web Server from the command line by running jwebserver. Then it will start serving the directory (and its subdirectories) from which we run it.

The jwebserver is present in the bin folder of the JDK installation. In macOS, it is present in the folder /Library/Java/JavaVirtualMachines/jdk-18.jdk/Contents/Home/bin.

jwebserver

Note: In order to run the jwebserver command, we must have added the Java bin folder path (the above mentioned path) to the PATH environment variable like,

PATH=$PATH:$JAVA_HOME/bin

i.e., assuming JAVA_HOME is set to /Library/Java/JavaVirtualMachines/jdk-18.jdk/Contents/Home (on a macOS).

If not, we must use the full path to the jwebserver.

Okay, getting back… Once the web server has started, it runs in the foreground and will print a message to the console (i.e., the terminal in which we’ve run it).

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

It gives us the following information:

  • I’ve run the jwebserver from the /tmp/JavaDevcentral folder in mac. Thus it is serving that folder and all of its subdirectories.
  • It is running on the localhost (127.0.0.1) (a.k.a loopback address) and on port 8000 by default.

Accessing the web server and resource contents

We can access the content that is served by accessing http://127.0.0.1:8000 or http://localhost:8000 in a browser. It will list all the contents of the directory which it is serving. 

jwebserver-directory-listing

jwebserver-directory-listing

It shows the files and directories (as links) from the folder which the web server is serving. Now, we can access the files and directories via the browser by clicking on them.

For example, clicking on java-notes.txt will take us to http://127.0.0.1:8000/java-notes.txt (or http://localhost:8000/java-notes.txt) and will show the file contents in the browser. 

Clicking on a folder will take us into that folder and will display its contents. Clicking on GoogleGuava folder takes us to http://127.0.0.1:8000/GoogleGuava and shows,

jwebserver-nested-directory-listing

jwebserver-nested-directory-listing

Stopping the web server from command line

The server will keep on running till we stop it. On unix platforms, we can stop the server by running Ctrl + C in the terminal window (which sends a SIGINT signal).

Logging output on the console

Whenever we access some resource (path) on the web server, it logs the request with some information on the console (System.out). When we access the root path (http://localhost:8000), we can see the below message printed,

127.0.0.1 - - [22/Mar/2022:10:36:34 +0530] "GET / HTTP/1.1" 200 -

By default, the logging output level is set to INFO, and it uses the Common Logfile Format. Its format is as follows.

remotehost rfc931 authuser [date] "request" status bytes

It prints the rfc931 (the remote logname of the user), authuser and bytes as – (hyphen). 

The rest of the information it captures are:

  • The localhost address in which the server is running.
  • Date and time of the request.
  • The request as it came from the client. This includes:
    • The HTTP verb of the request (GET)
    • The path that was accessed (/ in this case) 
    • HTTP version
  • The status code of the response.

If we access a file – say java-notes.txt through http://localhost:8000/java-notes.txt, then it logs, 

127.0.0.1 - - [22/Mar/2022:10:38:34 +0530] "GET /java-notes.txt HTTP/1.1" 200 -

Supported HTTP verbs

It only supports HEAD and GET HTTP verbs. Requests with other verbs (POST, PUT, DELETE, CONNECT, OPTIONS, TRACE or PATCH) will return a HTTP 405 (Method Not Allowed). Any other custom verb will return HTTP 501 (Not Implemented).

Let us try to make an OPTIONS call via curl (or you can use any REST API client like Postman or Insomnia as well).

curl -i --request OPTIONS 
  --url http://localhost:8000/
HTTP/1.1 405 Method Not Allowed
Date: <some_date>
Allow: HEAD, GET
Content-length: 0

It returned HTTP 405 Method Not Allowed response. The response has a response header called Allow, which specifies the list of supported headers.

If we use some custom verb, it returns HTTP 501 Not Implemented as shown below.

curl -i --request CUSTOM_VERB 
  --url http://localhost:8000/
HTTP/1.1 501 Not Implemented
Date: <some_date>
Content-length: 0

Symbolic links and hidden files

It doesn’t list or serve symbolic links or hidden files. Let us create a symbolic link named ‘link-to-java8-stream-notes’ in /tmp/JavaDevCentral folder pointing to the stream-notes.txt in Java8 folder.

ln -s /tmp/JavaDevCentral/Java8/stream-notes.txt link-to-java8-stream-notes

Now, if we access the root path (/), the symlink link-to-java8-stream-notes won’t show up. We cannot access it directly (http://localhost:8000/link-to-java8-stream-notes) as well. This will return 404 with a “File not found” message.

The same is true for hidden files. If we create a hidden file like,

touch .hidden-file.txt

It doesn’t show up in the web server directory listings and cannot be accessed directly as well (http://localhost:8000/hidden-file.txt will return a HTTP 404).

MIME type

The web server will automatically set the correct MIME type.
When accessing the root folder (or a html file), the Content-Type HTTP response header is set to text/html.

curl -I --request GET 
  --url http://localhost:8000
HTTP/1.1 200 OK
Date: <some_date>
Last-modified: <some_date>
Content-type: text/html; charset=UTF-8
Content-length: 368

If we access a text file, the Content-Type HTTP response header will be text/plain.

 curl -I --request GET 
  --url http://localhost:8000/java-notes.txt
HTTP/1.1 200 OK
Date: <some_date>
Last-modified: <some_date>
Content-type: text/plain
Content-length: 26

Other options available when running the web server

So far, we have explored the Java 18 simple web server utility by using the default options/behaviour. But it also provides a lot of options we can use on the command line when we start the web server. Let us look at them now.

Changing the address

By default, it runs binding to the loopback address or the localhost. We can change this to bind to all interfaces by passing -b option with 0.0.0.0. This will allow the server to listen on all available interfaces.

jwebserver -b 0.0.0.0

This starts the web server which prints,

Serving /private/tmp/JavaDevCentral and subdirectories on 0.0.0.0 (all interfaces) port 8000
URL http://192.168.1.5:8000/

For more context on 0.0.0.0, see,

  • What is the difference between 0.0.0.0, 127.0.0.1 and localhost?
  • What’s the difference between 127.0.0.1 and 0.0.0.0?

Now the server will be accessible at http://192.168.1.5:8000. But we can access it via localhost as well (as we have done before).

Changing the port

By default, it runs the server on port 8000. To run the server on a different port, we can pass it via the -p option (or the long option name –port). The below starts the web server on port 8080.

jwebserver -p 8080

Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /private/tmp/JavaDevCentral and subdirectories on 127.0.0.1 port 8080
URL http://127.0.0.1:8080/

Serving a different directory

By default it will serve the directory from which we run the jwebserver command. If we want to serve a different directory, we can use the -d option (the long name is –directory).

Let’s say we want to host/serve the Java8 folder. We can run the below from any directory. It will start the server which serves the folder which we have passed to the -d option.

jwebserver -d /tmp/JavaDevCentral/Java8

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

Accessing http://localhost:8000 now shows the contents of the Java8 folder.

jwebserver-custom-directory-listing

jwebserver-custom-directory-listing

Configuring the output (logging) level

As we have seen earlier, by default, it uses a logging level of INFO. There are two more options which we can use through the -o option name (–output).

No logging

If we don’t want to log anything, then we can pass the log/output level as none.

jwebserver -o none

In this configuration, the server will not log anything to the terminal console when we access the resources.

Verbose logging

We can configure a verbose logging output, which will include the request and response headers and the absolute path of the requested resource.

jwebserver -o verbose

An example is shown below accessing java-notes.txt (not all headers are shown).

127.0.0.1 - - [22/Mar/2022:10:40:26 +0530] "GET /java-notes.txt HTTP/1.1" 200 -
Resource requested: /private/tmp/JavaDevCentral/java-notes.txt
> Accept-encoding: gzip, deflate, br
> Sec-fetch-dest: document
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
> Sec-fetch-user: ?1
> Referer: http://localhost:8000/
> Connection: keep-alive
> Sec-fetch-site: same-origin
> Host: localhost:8000
> Accept-language: en-US,en;q=0.9
> ... other headers...
>
< Date: <some_date>
< Last-modified: <some_date>
< Content-type: text/plain
< Content-length: 26
<

Getting help and version

To get help on options, use -h option (–help) which shows all options with descriptions. We can get the version of jwebserver by running -version (or –version)

jwebserver -version
jwebserver 18

Conclusion

This post covered the Java 18 simple web server command line tool. We learnt how to start a web server from the command line and use it to serve static files. We also learnt about the configuration options it has.

I hope the post would have been helpful. And make sure to follow me on Twitter and/or subscribe to my newsletter to get future updates on the blog posts. Check out the java8 category for Java 8 classes and features.

References

  • JEP 408: Simple Web Server
  • HTTP request methods

One of the most frequency used protocol in the whole Internet *

* In OSI model, layer 7

Every time you visit a website your web browser uses the HTTP protocol to communicate with web server and fetch the page’s content. Also, when you are implementing backend app and you have to communicate with other backend app — 80% (or more) of cases you will use the HTTP.

Long story short — when you want to be a good software developer, you have to know how the HTTP protocol works. And wiring the HTTP server is pretty good way to understood, I think.

What a web browser sends to the web server?

Good question. Of course, you can use «developer tools», let’s do it.

Alt Text

Hmm… but what now? What exactly it means? We can see some URL, some method, some status, version (huh?), headers, and other stuff. Useful? Yes, but only to analyze the web app, when something is wrong. We still don’t know how HTTP works.

Wireshark, my old friend

The source of truth. Wireshark is application to analyze network traffic. You can use it to see each packet that is sent by your (or to your) PC.

Alt Text

But to be honest — if you know how to use Wireshark — you probably know how HTTP and TCP works. It’s pretty advanced program.

You are right — the specification

Every good (I mean — used by more that 5 peoples) protocols should have specification. In this case it’s called RFC. But don’t lie — you will never read this, it’s too long — https://tools.ietf.org/html/rfc2616 .

Just run the server and test

Joke? No. Probably you have installed on your PC very powerful tool called netcat, it’s pretty advanced tool.
One of the netcat features is TCP server. You can run the netcat to listen on specific port and print every thing what it gets. Netcat is a command line app.

nc -v -l -p 8080

Enter fullscreen mode

Exit fullscreen mode

Netcat (nc), please, listen (-l) on port 8080 (-p 8080) and print everything (-v).

Now open web browser and enter http://localhost:8080/. Your browser will send the HTTP request to the server runned by netcat. Of course nc will print the whole request and ignore it, browser will wait for the response (will timeout soon). To kill nc press ctrl+c.

asciicast

So, finally, we have an HTTP request!

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: JSESSIONID=D3AF43EBFC0C9D92AD9C37823C4BB299
Upgrade-Insecure-Requests: 1

Enter fullscreen mode

Exit fullscreen mode

As you can see — it fully texts protocol. No bits to analyze, just plain text.

HTTP request

It may be a little confusing. Maybe nc parses the request before printing? HTTP protocol should be complicated, where is the sequence of 0 and 1? There aren’t any. HTTP is really very simple text protocol. There is only one, little trap (I will explain it at the end of this section).

We can split the request to the 4 main parts:

GET / HTTP/1.1

Enter fullscreen mode

Exit fullscreen mode

This is the main request.

GET — this is the HTTP method. Probably you know there are a lot of methods.
GET means give me

/ — resource. / means default one.
When you will open localhost:8080/my_gf_nudes.html, the resource will be /my_gf_nudes.html

HTTP/1.1 — HTTP version. There are few versions, 1.1 is commonly used.

Host: localhost:8080

Enter fullscreen mode

Exit fullscreen mode

Host. One server can host many domains, using this field, the browser says which domain it wants exactly

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: JSESSIONID=D3AF43EBFC0C9D92AD9C37823C4BB299
Upgrade-Insecure-Requests: 1

Enter fullscreen mode

Exit fullscreen mode

Headers. In short: some additional informations. But I’m sure that you know what headers are :)


Enter fullscreen mode

Exit fullscreen mode

Surprise — empty line. It means: end of the request. In general — empty line in HTTP means end of section.

The trap

In HTTP every new line separator is a Window’s new line. rn not n. Remember.

Response

Ok. We have a request. How does response look like? Send a request to any server and see, there is nothing simpler.
On your laptop you can find another very useful tool — telnet. Using telenet you can open TCP connection, write something to server and print the response.
Try to do it yourself. Run telnet google.com 80 (80 is the default HTTP port) and type request manually (you know how it should look like). To close connection press ctrl+] than type quit.

asciicast

OK. We have a response.

HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Wed, 25 Mar 2020 18:53:12 GMT
Expires: Fri, 24 Apr 2020 18:53:12 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

Enter fullscreen mode

Exit fullscreen mode

We can split it to 4 sections

HTTP/1.1 301 Moved Permanently

Enter fullscreen mode

Exit fullscreen mode

HTTP/1.1 — version
301 — status code. I believe you are familiar with that
Moved Permanently — human-readable status code

Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Wed, 25 Mar 2020 18:53:12 GMT
Expires: Fri, 24 Apr 2020 18:53:12 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN

Enter fullscreen mode

Exit fullscreen mode

Headers


Enter fullscreen mode

Exit fullscreen mode

Empty line, it means that the content will be sent in next section.

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

Enter fullscreen mode

Exit fullscreen mode

Content, HTML or binary or something


Enter fullscreen mode

Exit fullscreen mode

Empty line, means end of request.

REMEMBER: each new line is rn

Time for programming!

We know how request look like, we know how response look like, it’s time to implement our server.

What we expect

We want to get a very simple thing — to display an HTML page and a picture in a browser.
Let’s prepare two HTMLs files and one picture

❯ pwd
/tmp/www

❯ ls
gallery.html index.html   me.jpg

❯ cat index.html
<html>
  <header>
    <title>My homepage!</title>
  </header>
  <body>
    <h1>Welcome!</h1>
    <p><a href="gallery.html">Here</a> you can look at my pictures</p>
  </body>
</html>

❯ cat gallery.html
<html>
  <head>
    <title>Gallery</title>
  </head>
  <body>
    <h1>My sexi photos<h1>
    <img src="me.jpg" />
  </body>
</html>

❯

Enter fullscreen mode

Exit fullscreen mode

The plan

Plan is very simple:

  1. Open TCP socket and listen
  2. Accept the client and read request
  3. Parse the request
  4. Find requested resource on disk
  5. Send the response
  6. Test

Open TCP socket

In this article we will use ServerSocket class to handle TCP connection. As a homework you can reimplement the server to use the classes from the nio packages.

So, open your IDE and let’s start.

    public static void main( String[] args ) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            while (true) {
                // implement client handler here
            }
        }
    }

Enter fullscreen mode

Exit fullscreen mode

I want to keep the code concise and clean — it’s why I throws Exception instead of implementing good exception handling.
So as I told, we have to open socket on port 8080 (why not 80? Because to use low port you need root privileges).
We also need the infinity loop to ‘pause the server’.

User telnet to test the socket:
Alt Text
Perfect, works.

Accept client connection

        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            while (true) {
                try (Socket client = serverSocket.accept()) {
                    handleClient(client);
                }
            }
        }

Enter fullscreen mode

Exit fullscreen mode

To accept connection from client we have to call blocking accept() method. Java program will wait for a client on that line.

Time to implement the client handler:

    private static void handleClient(Socket client) throws IOException {
        System.out.println("Debug: got new client " + client.toString());
        BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));

        StringBuilder requestBuilder = new StringBuilder();
        String line;
        while (!(line = br.readLine()).isBlank()) {
            requestBuilder.append(line + "rn");
        }

        String request = requestBuilder.toString();
        System.out.println(request);
    }

Enter fullscreen mode

Exit fullscreen mode

We have to read the request. How? Just read the input stream from the client’s socket. In Java it’s not so simple, that’s why I made this ugly line

new BufferedReader(new InputStreamReader(client.getInputStream()));

Enter fullscreen mode

Exit fullscreen mode

Well, Java.

Request ends with one empty line (rn), remember? Client will send empty line, but imputStream will be still open, we have to read it until one, empty line arrives.

Run the server, go to http://localhost:8080/ and observe logs:
Alt Text
It works! We can log the whole request!

Parse the request

Parsing the request is realy simple, I don’t think there’s any need for further explanation

        String request = requestBuilder.toString();
        String[] requestsLines = request.split("rn");
        String[] requestLine = requestsLines[0].split(" ");
        String method = requestLine[0];
        String path = requestLine[1];
        String version = requestLine[2];
        String host = requestsLines[1].split(" ")[1];

        List<String> headers = new ArrayList<>();
        for (int h = 2; h < requestsLines.length; h++) {
            String header = requestsLines[h];
            headers.add(header);
        }

        String accessLog = String.format("Client %s, method %s, path %s, version %s, host %s, headers %s",
                client.toString(), method, path, version, host, headers.toString());
        System.out.println(accessLog);

Enter fullscreen mode

Exit fullscreen mode

Just some splits. The only one thing that you may not understand is why we started the loop from 2? Because first line (index 0) is GET / HTTP/1.1, second line is host. The headers start with the third line of the request

Send response

We will send the response to the client’s output stream.

        OutputStream clientOutput = client.getOutputStream();
        clientOutput.write("HTTP/1.1 200 OKrn".getBytes());
        clientOutput.write(("ContentType: text/htmlrn").getBytes());
        clientOutput.write("rn".getBytes());
        clientOutput.write("<b>It works!</b>".getBytes());
        clientOutput.write("rnrn".getBytes());
        clientOutput.flush();
        client.close();

Enter fullscreen mode

Exit fullscreen mode

Do you remember how response should look like?

version status code
headers
(empty line)
content
(empty line)

Enter fullscreen mode

Exit fullscreen mode

Don’t forget to close the output stream.

Alt Text
Wow, it’s really works

Find requested resource

We have to implement two methods first

    private static String guessContentType(Path filePath) throws IOException {
        return Files.probeContentType(filePath);
    }

    private static Path getFilePath(String path) {
        if ("/".equals(path)) {
            path = "/index.html";
        }

        return Paths.get("/tmp/www", path);
    }

Enter fullscreen mode

Exit fullscreen mode

guessContentType — we have to tell to the browser what kind of content we are sending. It’s callend content type. Fortunately, there are built-in mechanisms in Java for this. We don’t have to make a big switch block.

getFilePath — Before we will return the file — we need to known it location.
This condition deserves attention

        if ("/".equals(path)) {
            path = "/index.html";
        }

Enter fullscreen mode

Exit fullscreen mode

If user wants default resource then return index.html.

send the response

Do you remember the code that sends response to the user (block of clientOutput.write)? We need to move it to the method

    private static void sendResponse(Socket client, String status, String contentType, byte[] content) throws IOException {
        OutputStream clientOutput = client.getOutputStream();
        clientOutput.write(("HTTP/1.1 rn" + status).getBytes());
        clientOutput.write(("ContentType: " + contentType + "rn").getBytes());
        clientOutput.write("rn".getBytes());
        clientOutput.write(content);
        clientOutput.write("rnrn".getBytes());
        clientOutput.flush();
        client.close();
    }

Enter fullscreen mode

Exit fullscreen mode

Ok, finally we can return the file

        Path filePath = getFilePath(path);
        if (Files.exists(filePath)) {
            // file exist
            String contentType = guessContentType(filePath);
            sendResponse(client, "200 OK", contentType, Files.readAllBytes(filePath));
        } else {
            // 404
            byte[] notFoundContent = "<h1>Not found :(</h1>".getBytes();
            sendResponse(client, "404 Not Found", "text/html", notFoundContent);
        }

Enter fullscreen mode

Exit fullscreen mode

It works!

Alt Text
Finally, we can see html page served by our web server!

Homework

  1. Make it multi-thread.
    1. Create thread pool
    2. Move handleClient method to separated class and run it in new thread
  2. Rewrite it using non-blocking IO
  3. Implement POST method
    1. Start netcat
    2. Send some HTML form
    3. Analyze request

Full source code

It’s possible to create an httpserver that provides basic support for J2EE servlets with just the JDK and the servlet api in a just a few lines of code.

I’ve found this very useful for unit testing servlets, as it starts much faster than other lightweight containers (we use jetty for production).

Most very lightweight httpservers do not provide support for servlets, but we need them, so I thought I’d share.

The below example provides basic servlet support, or throws and UnsupportedOperationException for stuff not yet implemented. It uses the com.sun.net.httpserver.HttpServer for basic http support.

import java.io.*;
import java.lang.reflect.*;
import java.net.InetSocketAddress;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.*;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

@SuppressWarnings("deprecation")
public class VerySimpleServletHttpServer {
    HttpServer server;
    private String contextPath;
    private HttpHandler httpHandler;

    public VerySimpleServletHttpServer(String contextPath, HttpServlet servlet) {
        this.contextPath = contextPath;
        httpHandler = new HttpHandlerWithServletSupport(servlet);
    }

    public void start(int port) throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(port);
        server = HttpServer.create(inetSocketAddress, 0);
        server.createContext(contextPath, httpHandler);
        server.setExecutor(null);
        server.start();
    }

    public void stop(int secondsDelay) {
        server.stop(secondsDelay);
    }

    public int getServerPort() {
        return server.getAddress().getPort();
    }

}

final class HttpHandlerWithServletSupport implements HttpHandler {

    private HttpServlet servlet;

    private final class RequestWrapper extends HttpServletRequestWrapper {
        private final HttpExchange ex;
        private final Map<String, String[]> postData;
        private final ServletInputStream is;
        private final Map<String, Object> attributes = new HashMap<>();

        private RequestWrapper(HttpServletRequest request, HttpExchange ex, Map<String, String[]> postData, ServletInputStream is) {
            super(request);
            this.ex = ex;
            this.postData = postData;
            this.is = is;
        }

        @Override
        public String getHeader(String name) {
            return ex.getRequestHeaders().getFirst(name);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            return new Vector<String>(ex.getRequestHeaders().get(name)).elements();
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            return new Vector<String>(ex.getRequestHeaders().keySet()).elements();
        }

        @Override
        public Object getAttribute(String name) {
            return attributes.get(name);
        }

        @Override
        public void setAttribute(String name, Object o) {
            this.attributes.put(name, o);
        }

        @Override
        public Enumeration<String> getAttributeNames() {
            return new Vector<String>(attributes.keySet()).elements();
        }

        @Override
        public String getMethod() {
            return ex.getRequestMethod();
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return is;
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        @Override
        public String getPathInfo() {
            return ex.getRequestURI().getPath();
        }

        @Override
        public String getParameter(String name) {
            String[] arr = postData.get(name);
            return arr != null ? (arr.length > 1 ? Arrays.toString(arr) : arr[0]) : null;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return postData;
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return new Vector<String>(postData.keySet()).elements();
        }
    }

    private final class ResponseWrapper extends HttpServletResponseWrapper {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final ServletOutputStream servletOutputStream = new ServletOutputStream() {

            @Override
            public void write(int b) throws IOException {
                outputStream.write(b);
            }
        };

        private final HttpExchange ex;
        private final PrintWriter printWriter;
        private int status = HttpServletResponse.SC_OK;

        private ResponseWrapper(HttpServletResponse response, HttpExchange ex) {
            super(response);
            this.ex = ex;
            printWriter = new PrintWriter(servletOutputStream);
        }

        @Override
        public void setContentType(String type) {
            ex.getResponseHeaders().add("Content-Type", type);
        }

        @Override
        public void setHeader(String name, String value) {
            ex.getResponseHeaders().add(name, value);
        }

        @Override
        public javax.servlet.ServletOutputStream getOutputStream() throws IOException {
            return servletOutputStream;
        }

        @Override
        public void setContentLength(int len) {
            ex.getResponseHeaders().add("Content-Length", len + "");
        }

        @Override
        public void setStatus(int status) {
            this.status = status;
        }

        @Override
        public void sendError(int sc, String msg) throws IOException {
            this.status = sc;
            if (msg != null) {
                printWriter.write(msg);
            }
        }

        @Override
        public void sendError(int sc) throws IOException {
            sendError(sc, null);
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            return printWriter;
        }

        public void complete() throws IOException {
            try {
                printWriter.flush();
                ex.sendResponseHeaders(status, outputStream.size());
                if (outputStream.size() > 0) {
                    ex.getResponseBody().write(outputStream.toByteArray());
                }
                ex.getResponseBody().flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                ex.close();
            }
        }
    }

    public HttpHandlerWithServletSupport(HttpServlet servlet) {
        this.servlet = servlet;
    }

    @SuppressWarnings("deprecation")
    @Override
    public void handle(final HttpExchange ex) throws IOException {
        byte[] inBytes = getBytes(ex.getRequestBody());
        ex.getRequestBody().close();
        final ByteArrayInputStream newInput = new ByteArrayInputStream(inBytes);
        final ServletInputStream is = new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return newInput.read();
            }
        };

        Map<String, String[]> parsePostData = new HashMap<>();

        try {
            parsePostData.putAll(HttpUtils.parseQueryString(ex.getRequestURI().getQuery()));

            // check if any postdata to parse
            parsePostData.putAll(HttpUtils.parsePostData(inBytes.length, is));
        } catch (IllegalArgumentException e) {
            // no postData - just reset inputstream
            newInput.reset();
        }
        final Map<String, String[]> postData = parsePostData;

        RequestWrapper req = new RequestWrapper(createUnimplementAdapter(HttpServletRequest.class), ex, postData, is);

        ResponseWrapper resp = new ResponseWrapper(createUnimplementAdapter(HttpServletResponse.class), ex);

        try {
            servlet.service(req, resp);
            resp.complete();
        } catch (ServletException e) {
            throw new IOException(e);
        }
    }

    private static byte[] getBytes(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        while (true) {
            int r = in.read(buffer);
            if (r == -1)
                break;
            out.write(buffer, 0, r);
        }
        return out.toByteArray();
    }

    @SuppressWarnings("unchecked")
    private static <T> T createUnimplementAdapter(Class<T> httpServletApi) {
        class UnimplementedHandler implements InvocationHandler {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                throw new UnsupportedOperationException("Not implemented: " + method + ", args=" + Arrays.toString(args));
            }
        }

        return (T) Proxy.newProxyInstance(UnimplementedHandler.class.getClassLoader(),
                new Class<?>[] { httpServletApi },
                new UnimplementedHandler());
    }
}

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

HTTPServer

Java SDK предоставляет встроенный сервер под названием HttpServer. Этот класс относится к пакету com.sun.net

Мы можем создать экземпляр сервера следующим образом:

HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8001), 0);

Приведенная выше строка создает экземпляр HTTPServer на локальном узле с номером порта 8001. Но есть еще один аргумент со значением 0. Это значение используется для обратной регистрации .

Очередь выполнения запросов

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

Код сервера

Мы собираемся разработать следующий HTTP-сервер:

server.createContext("/test", new  MyHttpHandler());
server.setExecutor(threadPoolExecutor);
server.start();
logger.info(" Server started on port 8001");

Мы создали контекст под названием test. Это не что иное, как корень контекста приложения. Второй параметр — это экземпляр обработчика, который будет обрабатывать HTTP-запросы. Мы рассмотрим этот класс в ближайшее время.

Мы можем использовать исполнителя пула потоков вместе с этим экземпляром сервера. В нашем случае мы создали пул из 10 потоков.

ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);

Далее запускаем сервер:

С помощью всего трех-четырех строк кода мы создали HTTP-сервер с корневым контекстом, который прослушивает порт!

HTTPHandler

Это интерфейс с вызванным методом handle(..) . Давайте посмотрим на нашу реализацию этого интерфейса.

private class MyHttpHandler implements HttpHandler {    

    @Override    
    public void handle(HttpExchange httpExchange) throws IOException {
        String requestParamValue=null; 

        if("GET".equals(httpExchange.getRequestMethod())) { 
            requestParamValue = handleGetRequest(httpExchange);
        } else if("POST".equals(httpExchange)) { 
            requestParamValue = handlePostRequest(httpExchange);        
        }  

        handleResponse(httpExchange,requestParamValue); 
    }

    private String handleGetRequest(HttpExchange httpExchange) {
        return httpExchange.
            getRequestURI()
            .toString()
            .split("\?")[1]
            .split("=")[1];
    }

    private void handleResponse(HttpExchange httpExchange, String requestParamValue)  throws  IOException {
        OutputStream outputStream = httpExchange.getResponseBody();
        StringBuilder htmlBuilder = new StringBuilder();

        htmlBuilder.append("").
            append("").
            append("

"). append("Hello ") .append(requestParamValue) .append("

") .append("") .append(""); // encode HTML content String htmlResponse = StringEscapeUtils.escapeHtml4(htmlBuilder.toString()); // this line is a must httpExchange.sendResponseHeaders(200, htmlResponse.length()); outputStream.write(htmlResponse.getBytes()); outputStream.flush(); outputStream.close(); } }

Это код, который обрабатывает запрос и отправляет ответ клиенту. Запрос и ответ обрабатываются классом HttpExchange.

Обработка запроса GET

Запрос GET обрабатывается методом handleGETRequest(). Этот метод, в свою очередь, вызывает метод getRequestURI() класса HttpExchange для извлечения значения параметра запроса, содержащегося в URI. Это минимальный метод, который будет обрабатывать только один параметр, присутствующий в запросе. Тем не менее, это может быть изменено для удовлетворения различных требований.

Обработка ответа

Наконец, мы собираемся отправить наш ответ обратно клиенту. Это достигается методом handleResponse(..). В этом методе мы получаем выходной поток, вызывая метод getResponseBody() класса HttpExchange. Позже мы можем записать содержимое HTML в выходной поток. 

Наиболее важным моментом является отправка response header обратно клиенту. Если вы пропустите это, вы получите сообщение об ошибке в браузере ERR_EMPTY_RESPONSE.

Если все идет хорошо, вы можете увидеть ответ в браузере запросив URL: http://localhost:8001/test?name=sam

Понравилась статья? Поделить с друзьями:
  • Как написать простое приложение на java
  • Как написать простое приложение для андроид
  • Как написать простое веб приложение
  • Как написать простого бота на python
  • Как написать проститутке