Несколько месяцев назад помимо собственно разработки я занялся также преподаванием, и возник вопрос о том, как проще и понятнее рассказать о сути протокола 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)
multiadmin
21.02.2019 14:05Слишком простой пример, что бы называться «Простейшим HTTP-сервером на Java».
Если хотите показать, как работают сокеты, что сделайте чат на сокетах.
Если хотите рассказать про HTTP, то нужно рассказывать про стандарты и реализации, иначе вы сделаете для студентов только хуже, упростив пример ниже допустимого минимума.usharik Автор
21.02.2019 14:07Скорее, заголовок не очень удачный. Мне хотелось не разбирать в очередной раз спецификацию протокола HTTP, а показать, что за всем этим стоит не более чем пересылка текстовых сообщений определенного содержания.
multiadmin
21.02.2019 14:27что за всем этим стоит не более чем пересылка текстовых сообщений определенного содержания
Ну это же неверно.
Вся суть HTTP — именно в спецификациях. Посмотрите, например, как лишь малую часть спецификации обсуждают в этой этой недавней теме REST страсти по 200.
Это опасно, чрезмерно упрощать тему и ставить акценты совершенно на другой уровень. Это сильно вводит в заблуждение.
Если хотите показать работу с сокетами, то сделайте простой чат.
Если ходите рассказать сетевую модель, то покажите это: ru.wikipedia.org/wiki/Сетевая_модель_OSI
Vantela
21.02.2019 14:20Ох, чат. Я как раз на Java решил написать чат, с клиентом и сервером.
И работы сокетов там на несколько строк. Все остальное это огромная обертка, которую я никак не могу заставить себя продолжить писать.
Пример выше как раз отлично показывает работу сокетов «на один экран».multiadmin
21.02.2019 14:33Пример выше как раз отлично показывает работу сокетов «на один экран».
Если хочется показать работу сокетов на примере HTTP, то нужно делать «простейший HTTP/1.0-клиент», а не сервер.
А преподавать что-то не зная сути вопроса я бы вообще не рискнул. Больше будет вреда, чем пользы.Vantela
21.02.2019 14:41Возможно, по этому то мы с вами и не преподаем :-)
Ну, тут то как раз логика понятна. Напишем крошечный сервер, а потом такой же клиент для него. Чтобы так сказать «полное покрытие было».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/networkusharik Автор
21.02.2019 15:14+1Логика была примерно такой, как вы пишете. Чем она плоха я не очень понимаю. И уж точно данный пример не отменяет необходимости изучать понятия и стандарты HTTP, но после подобного примера это изучение будет основано на понимании того, что происходит на самом деле. HTTP перестанет быть «абстрактным конем в вакууме».
multiadmin
21.02.2019 15:17Я про HTML вообще не упоминал.
HTTP перестанет быть «абстрактным конем в вакууме».
По моему мнению, это введение в заблуждение.
Если вы сделаете «Простой HTTP/1.0-клиент», то претензий к вам, по сути, не будет.
Если вы заявляете, что сделали «Простой HTTP-сервер», то это опасное введение в заблуждение.usharik Автор
21.02.2019 15:20Про HTML была опечатка. Думаю, что понял, что не так. Подумаю, как исправить.
usharik Автор
21.02.2019 14:35+1Собственно, в этом и была цель! А если сравнивать с чатом, то тут в качестве клиента выступает не какой-то ещё кусок кода, а такая всем хорошо известная вещь, как веб браузер.
usharik Автор
21.02.2019 16:24Разумеется я тут не первый кто что-то подобное пробует сделать)))))
habr.com/ru/post/69136
tuxi
21.02.2019 17:15+1По-моему вполне полезная статья. А если написать вторую часть, с примерами разбора и парсинга/фильтрации параметров, так совсем хорошо будет. Хоть джуны, которые кроме спринг-бута ничего больше и не видели, смогут посмотреть "как это сделано"
Mishiko
21.02.2019 17:40Чего то как то не современно (если конечно заголовок статьи соответствует ее содержимому), вроде начиная с Oracle JDK 1.6 есть класс HttpServer:
Пример HTTP-сервера на com.sun.net.httpserver.HttpServerimport 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(); } } }
usharik Автор
21.02.2019 17:56Суть статьи не в том, чтобы написать HTTP сервер, а в том, чтобы показать что у всех подобных серверов внутри. Очень и очень многие используют HttpServer, сервлеты и тому подобное даже близко не представляя, как все это работает.
tuxi
21.02.2019 20:10Продолжайте! На самом деле этой темы на 5 небольших статей легко хватит.
Мне понятно, что можно и не писать очередную статью, а отправить "гуглить" или смотреть исходники популярных библиотек…
Но тем и хорош хабр, что дает возможность на базе таких статей обсудить те или иные решения.usharik Автор
22.02.2019 13:05Есть мысль о цикле статей «Изучаем HTTP и пишем Web сервер». Надеюсь времени на это наскребу.
Googolplex
21.02.2019 23:34+2Стоит взять в привычку никогда не испольовать Reader/Writer обертки без явного указания кодировки:
// не надо так new InputStreamReader(socket.getInputStream()) // надо так new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)
Такой подход спасет от большого количества боли.
soul_survivor
Хороший велосипед уже давно есть github.com/NanoHttpd/nanohttpd Часто использую в небольших проектах и PoC
usharik Автор
В данном случае даже близко не стоит задача написать полнофункциональный http-сервер)) Тут просто иллюстрация основ того, как устроен любой http-сервер и как происходит взаимодействие по протоколу HTTP.
Throwable
Вот совсем не так устроен, даже не близко.
Для начала сделайте обработку запроса в отдельном треде: либо новом, либо из пула. Это добавит всего пару строк, но сделает ваш сервер многопользовательским. А то вы так бодро и оптимистично из сокета читаете, что как-будто кто-то обязан доставить вам незамедлительно весь http-запрос, а также моментально отправить ваш ответ. А вот треды исключат блокировку, и ваш сервер сразу станет актуальным на период до 2002 года.
В 2002 году вышла Java 1.4, в которой, наконец, запилили неблокирующий NIO, который предлагал совершенно другую модель взаимодействия. Треды стали ненужны, ну или не столь актуальны, но прогать стало на порядки сложнее. Поэтому Apache Mina или Netty.
usharik Автор
Поверьте, я про это знаю) Но в данном случае такой задачи даже близко не стояло. Задача была написать короткий и простой код, которого достаточно чтобы принять HTTP запрос и ответить на него так, чтобы браузер понял. Это материал для совсем новичков, кто раньше никогда не писал веб приложений.
usharik Автор
Но видимо текст не очень хорошо написан, если приходится в который раз это объяснять. Увы.
NeverIn
Как устроен сервер скрыто в ServerSocket
usharik Автор
Пока что туда мы не пойдём)