Первое знакомство с протоколом HTTP через написание простейшего Web сервера на Java

    Думаю что не будет преувеличением утверждать, что знание и понимание сути протокола 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.
    Поделиться публикацией

    Комментарии 41

      +1
      Хороший велосипед уже давно есть github.com/NanoHttpd/nanohttpd Часто использую в небольших проектах и PoC
        0
        В данном случае даже близко не стоит задача написать полнофункциональный http-сервер)) Тут просто иллюстрация основ того, как устроен любой http-сервер и как происходит взаимодействие по протоколу HTTP.
          +1

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


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

            0

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

              +1
              Так это ж для меня :)
              Вот Java Core курс пойду и обязательно вернусь!
              +1

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

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

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

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

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

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

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

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

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

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

                Если ходите рассказать сетевую модель, то покажите это: ru.wikipedia.org/wiki/Сетевая_модель_OSI
                  0
                  Благодарю за ссылки!
                0
                Ох, чат. Я как раз на Java решил написать чат, с клиентом и сервером.
                И работы сокетов там на несколько строк. Все остальное это огромная обертка, которую я никак не могу заставить себя продолжить писать.

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

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

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

                    Ну, тут то как раз логика понятна. Напишем крошечный сервер, а потом такой же клиент для него. Чтобы так сказать «полное покрытие было».
                      0
                      Полагаю, логика (ошибочная) была другая, а именно: что бы показать, что мы сделали полноценный 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
                        Логика была примерно такой, как вы пишете. Чем она плоха я не очень понимаю. И уж точно данный пример не отменяет необходимости изучать понятия и стандарты HTTP, но после подобного примера это изучение будет основано на понимании того, что происходит на самом деле. HTTP перестанет быть «абстрактным конем в вакууме».
                          +1
                          Я про HTML вообще не упоминал.

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

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

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

                          Если вы заявляете, что сделали «Простой HTTP-сервер», то это опасное введение в заблуждение.
                            0
                            Про HTML была опечатка. Думаю, что понял, что не так. Подумаю, как исправить.
                    +1

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

                      +1
                      В варианте чата можно telnet вместо клиента использовать.
                      Но скучно.
                        0
                        Именно)
                  0
                  Разумеется я тут не первый кто что-то подобное пробует сделать)))))
                  habr.com/ru/post/69136
                    +1

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

                      +1
                      Для них и писал))
                      0
                      не стесняйтесь использовать com.sun.net.httpserver
                        –2
                        А лучше сразу Spring Boot! Не об этом же статья)
                          0
                          Да, лучше сразу Spring Boot.
                            0

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

                              0
                              Что сказать, уел меня :)
                        0
                        Чего то как то не современно (если конечно заголовок статьи соответствует ее содержимому), вроде начиная с 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();
                                }
                            }
                        }
                        


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

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

                              0
                              Есть мысль о цикле статей «Изучаем HTTP и пишем Web сервер». Надеюсь времени на это наскребу.
                                0
                                А таких статей на Хабре точно не было?
                                  0
                                  Как-то мудрый коллега сказал мне, что одна из главных проблем нашей профессии в том, что почти все программы (статьи) уже написаны. Но повод ли это прекращать старания?)
                                    0
                                    В научных статьях принято читать «коллег» и дополнять то, что не освещено или описано было не так хорошо, как в новой пердлагаемой статье. Ведь это только улучшит материал и даст возможность новым читателям прочитать и другие статьи на данную тему.

                                    Например так старался делать Иван Головач в своих статьях: ivangolovach
                              0
                              Я наивно думал, что суть статьи соответствует заголовку)
                                0

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

                            +3

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


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

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

                              0

                              Благодарю!

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое