Думаю что не будет преувеличением утверждать, что знание и понимание сути протокола 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()));
                     PrintWriter output = new PrintWriter(socket.getOutputStream(), StandardCharsets.UTF_8)) {

                    // ждем первой строки запроса
                    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 через его реализацию.

Комментарии (34)


  1. soul_survivor
    21.02.2019 13:55

    Хороший велосипед уже давно есть github.com/NanoHttpd/nanohttpd Часто использую в небольших проектах и PoC


    1. usharik Автор
      21.02.2019 14:03

      В данном случае даже близко не стоит задача написать полнофункциональный http-сервер)) Тут просто иллюстрация основ того, как устроен любой http-сервер и как происходит взаимодействие по протоколу HTTP.


      1. Throwable
        21.02.2019 18:31

        Вот совсем не так устроен, даже не близко.
        Для начала сделайте обработку запроса в отдельном треде: либо новом, либо из пула. Это добавит всего пару строк, но сделает ваш сервер многопользовательским. А то вы так бодро и оптимистично из сокета читаете, что как-будто кто-то обязан доставить вам незамедлительно весь http-запрос, а также моментально отправить ваш ответ. А вот треды исключат блокировку, и ваш сервер сразу станет актуальным на период до 2002 года.


        В 2002 году вышла Java 1.4, в которой, наконец, запилили неблокирующий NIO, который предлагал совершенно другую модель взаимодействия. Треды стали ненужны, ну или не столь актуальны, но прогать стало на порядки сложнее. Поэтому Apache Mina или Netty.


        1. usharik Автор
          22.02.2019 00:21

          Поверьте, я про это знаю) Но в данном случае такой задачи даже близко не стояло. Задача была написать короткий и простой код, которого достаточно чтобы принять HTTP запрос и ответить на него так, чтобы браузер понял. Это материал для совсем новичков, кто раньше никогда не писал веб приложений.


        1. usharik Автор
          22.02.2019 00:22
          +1

          Но видимо текст не очень хорошо написан, если приходится в который раз это объяснять. Увы.


      1. NeverIn
        21.02.2019 21:21

        Как устроен сервер скрыто в ServerSocket


        1. usharik Автор
          22.02.2019 00:24

          Пока что туда мы не пойдём)


  1. multiadmin
    21.02.2019 14:05

    Слишком простой пример, что бы называться «Простейшим HTTP-сервером на Java».

    Если хотите показать, как работают сокеты, что сделайте чат на сокетах.

    Если хотите рассказать про HTTP, то нужно рассказывать про стандарты и реализации, иначе вы сделаете для студентов только хуже, упростив пример ниже допустимого минимума.


    1. usharik Автор
      21.02.2019 14:07

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


      1. multiadmin
        21.02.2019 14:27

        что за всем этим стоит не более чем пересылка текстовых сообщений определенного содержания

        Ну это же неверно.

        Вся суть HTTP — именно в спецификациях. Посмотрите, например, как лишь малую часть спецификации обсуждают в этой этой недавней теме REST страсти по 200.

        Это опасно, чрезмерно упрощать тему и ставить акценты совершенно на другой уровень. Это сильно вводит в заблуждение.

        Если хотите показать работу с сокетами, то сделайте простой чат.

        Если ходите рассказать сетевую модель, то покажите это: ru.wikipedia.org/wiki/Сетевая_модель_OSI


        1. usharik Автор
          21.02.2019 14:46

          Благодарю за ссылки!


    1. Vantela
      21.02.2019 14:20

      Ох, чат. Я как раз на Java решил написать чат, с клиентом и сервером.
      И работы сокетов там на несколько строк. Все остальное это огромная обертка, которую я никак не могу заставить себя продолжить писать.

      Пример выше как раз отлично показывает работу сокетов «на один экран».


      1. multiadmin
        21.02.2019 14:33

        Пример выше как раз отлично показывает работу сокетов «на один экран».

        Если хочется показать работу сокетов на примере HTTP, то нужно делать «простейший HTTP/1.0-клиент», а не сервер.

        А преподавать что-то не зная сути вопроса я бы вообще не рискнул. Больше будет вреда, чем пользы.


        1. Vantela
          21.02.2019 14:41

          Возможно, по этому то мы с вами и не преподаем :-)

          Ну, тут то как раз логика понятна. Напишем крошечный сервер, а потом такой же клиент для него. Чтобы так сказать «полное покрытие было».


          1. multiadmin
            21.02.2019 15:01

            Полагаю, логика (ошибочная) была другая, а именно: что бы показать, что мы сделали полноценный HTTP-сервер продемонстируем, что можем к нему обратиться из браузера (популярный браузер — это авторитетная вещь) и получить ответ.

            На самом деле, можно продемонстрировать работу клиента и «авторитетной вещи», например, обратившись к «google.com» по порту 80.

            Или на «ya.ru» с запросом:

            GET / HTTP/1.1
            Host: ya.ru


            И, включив отладку в браузере (Ctrl+Shift+I) и показав, что тоже самое видит и браузер.

            И для этого совсем не обязательно даже писать программу, достаточно использовать telnet.

            А упор при рассказе про HTTP нужно делать на понятия, стандарты, сценарии, демонстрации с помощью браузерного инструмента разработчика: developers.google.com/web/tools/chrome-devtools/network


            1. usharik Автор
              21.02.2019 15:14
              +1

              Логика была примерно такой, как вы пишете. Чем она плоха я не очень понимаю. И уж точно данный пример не отменяет необходимости изучать понятия и стандарты HTTP, но после подобного примера это изучение будет основано на понимании того, что происходит на самом деле. HTTP перестанет быть «абстрактным конем в вакууме».


              1. multiadmin
                21.02.2019 15:17

                Я про HTML вообще не упоминал.

                HTTP перестанет быть «абстрактным конем в вакууме».

                По моему мнению, это введение в заблуждение.

                Если вы сделаете «Простой HTTP/1.0-клиент», то претензий к вам, по сути, не будет.

                Если вы заявляете, что сделали «Простой HTTP-сервер», то это опасное введение в заблуждение.


                1. usharik Автор
                  21.02.2019 15:20

                  Про HTML была опечатка. Думаю, что понял, что не так. Подумаю, как исправить.


      1. usharik Автор
        21.02.2019 14:35
        +1

        Собственно, в этом и была цель! А если сравнивать с чатом, то тут в качестве клиента выступает не какой-то ещё кусок кода, а такая всем хорошо известная вещь, как веб браузер.


        1. Vantela
          21.02.2019 14:43

          В варианте чата можно telnet вместо клиента использовать.
          Но скучно.


          1. usharik Автор
            21.02.2019 14:45

            Именно)


  1. usharik Автор
    21.02.2019 16:24

    Разумеется я тут не первый кто что-то подобное пробует сделать)))))
    habr.com/ru/post/69136


  1. tuxi
    21.02.2019 17:15
    +1

    По-моему вполне полезная статья. А если написать вторую часть, с примерами разбора и парсинга/фильтрации параметров, так совсем хорошо будет. Хоть джуны, которые кроме спринг-бута ничего больше и не видели, смогут посмотреть "как это сделано"


    1. usharik Автор
      21.02.2019 17:24
      +1

      Для них и писал))


  1. ladutsko
    21.02.2019 17:38

    не стесняйтесь использовать com.sun.net.httpserver


    1. usharik Автор
      21.02.2019 17:56

      А лучше сразу Spring Boot! Не об этом же статья)


  1. Mishiko
    21.02.2019 17:40

    Чего то как то не современно (если конечно заголовок статьи соответствует ее содержимому), вроде начиная с Oracle JDK 1.6 есть класс HttpServer:

    Пример HTTP-сервера на com.sun.net.httpserver.HttpServer
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.InetSocketAddress;
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    import com.sun.net.httpserver.HttpServer;
    public class App {
        public static void main(String[] args) throws Exception {
            HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
            server.createContext("/test", new MyHandler());
            //Thread control is given to executor service.
            server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool());
            server.start();
        }
        static class MyHandler implements HttpHandler {
            @Override
            public void handle(HttpExchange t) throws IOException {
                String response = "This is the response";
                long threadId = Thread.currentThread().getId();
                System.out.println("I am thread " + threadId );
                response = response + "Thread Id = "+threadId;
                t.sendResponseHeaders(200, response.length());
                OutputStream os = t.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        }
    }
    


    1. usharik Автор
      21.02.2019 17:56

      Суть статьи не в том, чтобы написать HTTP сервер, а в том, чтобы показать что у всех подобных серверов внутри. Очень и очень многие используют HttpServer, сервлеты и тому подобное даже близко не представляя, как все это работает.


      1. tuxi
        21.02.2019 20:10

        Продолжайте! На самом деле этой темы на 5 небольших статей легко хватит.
        Мне понятно, что можно и не писать очередную статью, а отправить "гуглить" или смотреть исходники популярных библиотек…
        Но тем и хорош хабр, что дает возможность на базе таких статей обсудить те или иные решения.


        1. usharik Автор
          22.02.2019 13:05

          Есть мысль о цикле статей «Изучаем HTTP и пишем Web сервер». Надеюсь времени на это наскребу.


      1. Mishiko
        21.02.2019 20:55

        Я наивно думал, что суть статьи соответствует заголовку)


        1. usharik Автор
          21.02.2019 21:37

          Уже понял, что заголовок не удачен. Думаю над новым.


  1. Googolplex
    21.02.2019 23:34
    +2

    Стоит взять в привычку никогда не испольовать Reader/Writer обертки без явного указания кодировки:


    // не надо так
    new InputStreamReader(socket.getInputStream())
    // надо так
    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)

    Такой подход спасет от большого количества боли.


    1. usharik Автор
      22.02.2019 00:23

      Благодарю!