Рубрики
Без рубрики

Сервер Java Создайте свой собственный HTTP-сервер на Java менее чем за один час (только метод GET)

Один из наиболее часто используемых протоколов во всем Интернете * * В модели OSI уровень 7 E… Помеченный java, http.

* В модели OSI уровень 7

Каждый раз, когда вы посещаете веб-сайт, ваш веб-браузер использует протокол HTTP для связи с веб-сервером и извлечения содержимого страницы. Кроме того, когда вы внедряете серверное приложение и вам приходится взаимодействовать с другим серверным приложением – в 80% (или более) случаев вы будете использовать HTTP.

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

Хороший вопрос. Конечно, вы можете использовать “инструменты разработчика”, давайте сделаем это.

Хмм… но что теперь? Что именно это значит? Мы можем видеть какой-то URL, какой-то метод, какой-то статус, версию (да?), заголовки и прочее. Полезно? Да, но только для анализа веб-приложения, когда что-то не так. Мы до сих пор не знаем, как работает HTTP.

Wireshark, мой старый друг

Источник истины. Wireshark – это приложение для анализа сетевого трафика. Вы можете использовать его для просмотра каждого пакета, отправленного вашим (или на ваш) ПК.

Но, честно говоря, если вы знаете, как использовать Wireshark, вы, вероятно, знаете, как работают HTTP и TCP. Это довольно продвинутая программа.

Вы правы – спецификация

У каждого хорошего (я имею в виду – используемого более чем 5 людьми) протокола должна быть спецификация. В данном случае он называется RFC . Но не лги – ты никогда не прочтешь это, это слишком долго – https://tools.ietf.org/html/rfc2616 .

Просто запустите сервер и протестируйте

Шутка? Нет. Вероятно, вы установили на свой компьютер очень мощный инструмент под названием netcat, это довольно продвинутый инструмент. Одной из функций netcat является TCP-сервер. Вы можете запустить netcat для прослушивания определенного порта и распечатать все, что он получает. Netcat – это приложение командной строки.

nc -v -l -p 8080

Netcat ( nc ), пожалуйста, прослушайте ( -l ) порт 8080 ( -p 8080 ) и распечатайте все ( -v ).

Теперь откройте веб-браузер и введите http://localhost:8080/ . Ваш браузер отправит HTTP-запрос на сервер, управляемый netcat. Конечно, nc напечатает весь запрос и проигнорирует его, браузер будет ждать ответа (скоро наступит тайм-аут). Чтобы убить nc , нажмите ctrl+c .

Итак, наконец-то у нас есть HTTP-запрос!

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

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

Это может немного сбить с толку. Может быть, nc анализирует запрос перед печатью? Протокол HTTP должен быть сложным, где последовательность 0 и 1? Там их нет. HTTP – это действительно очень простой текстовый протокол. Есть только одна маленькая ловушка (я объясню ее в конце этого раздела).

Мы можем разделить запрос на 4 основные части:

GET / HTTP/1.1

Это основной запрос.

GET – это HTTP-метод. Наверное, вы знаете существует множество методов . ПОЛУЧИТЬ означает дать мне

/ – ресурс. / означает значение по умолчанию/| . Когда вы откроете localhost:8080/my_gf_nudes.html , ресурс будет /my_gf_nudes.html

HTTP/1.1 – 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

Заголовки. Вкратце: некоторая дополнительная информация. Но я уверен, что вы знаете, что такое заголовки:)

Сюрприз – пустая строка. Это означает: конец запроса. В общем случае – пустая строка в HTTP означает конец раздела .

Ловушка

В HTTP каждый разделитель новой строки – это новая строка окна. \r\n не \n . Помнить.

Хорошо. У нас есть просьба. Как выглядит ответ? Отправьте запрос на любой сервер и посмотрите, нет ничего проще. На вашем ноутбуке вы можете найти еще один очень полезный инструмент – telnet . Используя telnet, вы можете открыть TCP-соединение, записать что-то на сервер и распечатать ответ. Попробуйте сделать это сами. Выполнить telnet google.com 80 (80 – это HTTP-порт по умолчанию) и введите запрос вручную (вы знаете, как это должно выглядеть). Чтобы закрыть соединение, нажмите ctrl+] затем введите выйти .

ОК. У нас есть ответ.

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


301 Moved

301 Moved

The document has moved here.

Мы можем разделить его на 4 части

HTTP/1.1 301 Moved Permanently

HTTP/1.1 – версия 301код состояния . Я полагаю, вы знакомы с этим Перемещен навсегда – читаемый человеком код статуса

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

Заголовки

Пустая строка означает, что содержимое будет отправлено в следующем разделе.


301 Moved

301 Moved

The document has moved here.

Контент, HTML или двоичный файл или что-то в этом роде

Пустая строка, означает конец запроса .

помнить: каждая новая строка – это \r\n

Мы знаем, как выглядит запрос, мы знаем, как выглядит ответ, пришло время внедрить наш сервер.

Чего мы ожидаем

Мы хотим получить очень простую вещь – отобразить HTML-страницу и картинку в браузере. Давайте подготовим два Html-файла и одна картинка

❯ pwd
/tmp/www

❯ ls
gallery.html index.html   me.jpg

❯ cat index.html

  
My homepage!

Welcome!

Here you can look at my pictures

❯ cat gallery.html Gallery

My sexi photos

План

План очень прост:

  1. Откройте TCP-сокет и прослушайте
  2. Примите клиента и прочитайте запрос
  3. Проанализируйте запрос
  4. Найти запрошенный ресурс на диске
  5. Отправить ответ
  6. Тест

Открыть TCP-сокет

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

Итак, откройте свою среду разработки и давайте начнем.

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

Я хочу, чтобы код был кратким и чистым – вот почему я генерирую исключение вместо того, чтобы внедрять хорошую обработку исключений. Итак, как я уже сказал, мы должны открыть сокет на порту 8080 (почему не 80? Потому что для использования низкого порта вам нужны права суперпользователя). Нам также нужен бесконечный цикл, чтобы “приостановить сервер”.

Пользователь telnet для проверки сокета: Отлично, работает.

Принять клиентское соединение

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

Чтобы принять соединение от клиента, мы должны вызвать blocking метод accept() . Java-программа будет ждать клиента в этой строке.

Время для внедрения клиентского обработчика:

    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 + "\r\n");
        }

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

Мы должны прочитать запрос. Как? Просто считайте входной поток из клиентского сокета. В Java это не так просто, вот почему я сделал эту уродливую строку

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

Ну, Ява.

Запрос заканчивается одной пустой строкой ( \r\n ), помните? Клиент отправит пустую строку, но inputstream все еще будет открыт, мы должны прочитать его до тех пор, пока не появится одна пустая строка.

Запустите сервер, перейдите в http://localhost:8080/ и наблюдайте за журналами: Это работает! Мы можем зарегистрировать весь запрос целиком!

Проанализируйте запрос

Синтаксический анализ запроса действительно прост, я не думаю, что есть какая-либо необходимость в дальнейших объяснениях

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

        List 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);

Просто несколько расколов. Единственная вещь, которую вы, возможно, не понимаете, – это почему мы начали цикл с 2? Поскольку первая строка (индекс 0) – GET/HTTP/1.1 , вторая строка – host. Заголовки начинаются с третьей строки запроса

Отправить ответ

Мы отправим ответ в выходной поток клиента.

        OutputStream clientOutput = client.getOutputStream();
        clientOutput.write("HTTP/1.1 200 OK\r\n".getBytes());
        clientOutput.write(("ContentType: text/html\r\n").getBytes());
        clientOutput.write("\r\n".getBytes());
        clientOutput.write("It works!".getBytes());
        clientOutput.write("\r\n\r\n".getBytes());
        clientOutput.flush();
        client.close();

Вы помните, как должен выглядеть ответ?

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

Не забудьте закрыть выходной поток.

Вау, это действительно работает

Найти запрошенный ресурс

Сначала мы должны реализовать два метода

    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);
    }

guessContentType – мы должны сообщить браузеру, какой контент мы отправляем. Он называется content type . К счастью, в Java для этого есть встроенные механизмы. Нам не нужно делать большой блок переключения.

getFilePath – Прежде чем мы вернем файл – нам нужно знать его местоположение. Это условие заслуживает внимания

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

Если пользователь хочет ресурс по умолчанию , то верните index.html .

отправить ответ

Вы помните код, который отправляет ответ пользователю (блок client Output.write )? Нам нужно переместить его в метод

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

Хорошо, наконец-то мы можем вернуть файл

        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 = "

Not found :(

".getBytes(); sendResponse(client, "404 Not Found", "text/html", notFoundContent); }

Наконец-то мы можем увидеть html-страницу, обслуживаемую нашим веб-сервером!

  1. Сделайте его многопоточным.
    1. Создать пул потоков
    2. Двигаться handleClient метод для отдельного класса и запустите его в новом потоке
  2. Перепишите его, используя неблокирующий ввод-вывод
  3. Реализовать метод POST
    1. Запустите netcat
    2. Отправьте какую-нибудь HTML-форму
    3. Проанализировать запрос

Оригинал: “https://dev.to/mateuszjarzyna/build-your-own-http-server-in-java-in-less-than-one-hour-only-get-method-2k02”